File Coverage

blib/lib/Ado/Plugin/Auth.pm
Criterion Covered Total %
statement 98 200 49.0
branch 33 72 45.8
condition 10 30 33.3
subroutine 11 20 55.0
pod 5 5 100.0
total 157 327 48.0


line stmt bran cond sub pod time code
1             package Ado::Plugin::Auth;
2 23     23   29399 use Mojo::Base 'Ado::Plugin';
  23         51  
  23         193  
3 23     23   5280 use Mojo::Util qw(class_to_path);
  23         48  
  23         69700  
4              
5             sub register {
6 23     23 1 1803 my ($self, $app, $conf) = shift->initialise(@_);
7              
8             # Make sure we have all we need from config files.
9 23   50     137 $conf->{auth_methods} ||= ['ado'];
10 23         180 $app->helper(login_ado => \&_login_ado);
11              
12 23         553 $app->config(auth_methods => $conf->{auth_methods});
13 23         526 $app->config(ref($self) => $conf);
14              
15              
16             #OAuth2 providers
17 23         265 my @auth_methods = @{$conf->{auth_methods}};
  23         79  
18              
19 23 50       102 if (@auth_methods > 1) {
20 0         0 for my $m (@auth_methods) {
21 0 0       0 next if $m eq 'ado';
22             Carp::croak("Configuration options for authentication method \"$m\" "
23             . "are not enough!. Please add them.")
24 0 0       0 if (keys %{$conf->{providers}{$m}} < 2);
  0         0  
25             }
26 0         0 $app->plugin('OAuth2', {%{$conf->{providers}}, fix_get_token => 1});
  0         0  
27             }
28              
29             # Add helpers
30             #oauth2 links - helpers after 'ado'
31             $app->helper(login_google => \&_login_google)
32 23 50   23   132 if (List::Util::first { $_ eq 'google' } @auth_methods);
  23         95  
33             $app->helper(login_facebook => \&_login_facebook)
34 23 50   23   118 if (List::Util::first { $_ eq 'google' } @auth_methods);
  23         78  
35              
36             # Add conditions
37 23         100 $app->routes->add_condition(authenticated => \&authenticated);
38             $app->routes->add_condition(
39             ingroup => sub {
40 3 50   3   90618 $_[1]->debug("is user " . $_[1]->user->name . " in group $_[-1]?")
41             if $Ado::Control::DEV_MODE;
42 3         85 return $_[1]->user->ingroup($_[-1]);
43             }
44 23         438 );
45             $app->hook(
46             after_user_add => sub {
47 0     0   0 my ($c, $user, $raw_data) = @_;
48 0         0 $app->log->info($user->description . ' $user->id ' . $user->id . ' added!');
49 0 0       0 $c->debug('new user created with arguments:' . $c->dumper($user->data, $raw_data))
50             if $Ado::Control::DEV_MODE;
51             }
52 23         433 );
53             $app->hook(
54             after_login => sub {
55 2     2   36 my ($c) = @_;
56 2         9 $c->session(adobar_links => []);
57              
58             # Store a friendly message for the next page in flash
59 2         49 $c->flash(login_message => $c->l('LoginThanks'));
60             }
61 23         522 );
62 23         728 return $self;
63             }
64              
65              
66             # general condition for authenticating users - redirects to /login
67             sub authenticated {
68 5     5 1 61289 my ($route, $c) = @_;
69 5 50       51 $c->debug('in condition "authenticated"') if $Ado::Control::DEV_MODE;
70 5 100       121 if ($c->user->login_name eq 'guest') {
71 3         44 $c->session(over_route => $c->req->url);
72 3 50       119 $c->debug('session(over_route => $c->req->url):' . $c->session('over_route'))
73             if $Ado::Control::DEV_MODE;
74 3         100 $c->redirect_to('/login');
75 3         84 return;
76             }
77 2         26 return 1;
78             }
79              
80              
81             #expires the session.
82             sub logout {
83 1     1 1 10 my ($c) = @_;
84 1         6 $c->session(expires => 1);
85 1         22 $c->redirect_to('/');
86 1         47 return;
87             }
88              
89             #authenticate a user /login route implementation is here
90             sub login {
91 17     17 1 183 my ($c) = @_;
92              
93             #TODO: add json format
94              
95             #prepare redirect url for after login
96 17 100       127 unless ($c->session('over_route')) {
97 2         36 my $base_url = $c->url_for('/')->base;
98 2   66     474 my $referrer = $c->req->headers->referrer // $base_url;
99 2 50       86 $referrer = $base_url unless $referrer =~ m|^$base_url|;
100 2         401 $c->session('over_route' => $referrer);
101 2 50       51 $c->debug('over_route is ' . $referrer) if $Ado::Control::DEV_MODE;
102             }
103 17         272 my $auth_method = Mojo::Util::trim($c->param('auth_method'));
104              
105 17 100 100     539 return $c->render(status => 200, template => 'login')
106             if $c->req->method ne 'POST' && $auth_method eq 'ado';
107              
108             #derive a helper name for login the user
109 10         188 my $login_helper = 'login_' . $auth_method;
110 10 50       90 $c->debug('Chosen $login_helper: ' . $login_helper) if $Ado::Control::DEV_MODE;
111              
112 10         212 my $authnticated = 0;
113 10 100       31 if (eval { $authnticated = $c->$login_helper(); 1 }) {
  10         161  
  8         58  
114 8 100       23 if ($authnticated) {
115 2         8 $c->app->plugins->emit_hook(after_login => $c);
116              
117             # Redirect to referrer page with a 302 response
118 2 50       153 $c->debug('redirecting to ' . $c->session('over_route'))
119             if $Ado::Control::DEV_MODE;
120 2         49 $c->redirect_to($c->session('over_route'));
121 2         81 return;
122             }
123             else {
124 6 100 100     21 unless ($c->res->code // '' eq '403') {
125 5         91 $c->stash(error_login => 'Wrong credentials! Please try again!');
126 5         92 $c->render(status => 401, template => 'login');
127 5         254 return;
128             }
129             }
130             }
131             else {
132 2         4308 $c->app->log->error("Error calling \$login_helper:[$login_helper][$@]");
133 2         231 $c->stash(error_login => 'Please choose one of the supported login methods.');
134 2         67 $c->render(status => 401, template => 'login');
135 2         281 return;
136             }
137 1         29 return;
138             }
139              
140             #used as helper 'login_ado' returns 1 on success, '' otherwise
141             sub _login_ado {
142 8     8   246 my ($c) = @_;
143              
144             #1. do basic validation first
145 8         39 my $val = $c->validation;
146 8 100       1349 return '' unless $val->has_data;
147 7 100       94 if ($val->csrf_protect->has_error('csrf_token')) {
148 1         35 delete $c->session->{csrf_token};
149 1         18 $c->render(error_login => 'Bad CSRF token!', status => 403, template => 'login');
150 1         37 return '';
151             }
152 6         165 my $_checks = Ado::Model::Users->CHECKS;
153 6         29 $val->required('login_name')->like($_checks->{login_name}{allow});
154 6         719 $val->required('digest')->like(qr/^[0-9a-f]{40}$/);
155 6 100       459 if ($val->has_error) {
156 2         21 delete $c->session->{csrf_token};
157 2         30 return '';
158             }
159              
160             #2. find the user and do logical checks
161 4         42 my $login_name = $val->param('login_name');
162 4         70 my $user = Ado::Model::Users->by_login_name($login_name);
163 4 100 66     392 if ((not $user->id) or $user->disabled) {
164 1         5 delete $c->session->{csrf_token};
165 1         17 $c->stash(error_login_name => "No such user '$login_name'!");
166 1         21 return '';
167             }
168              
169             #3. really authnticate the user
170 3         387 my $checksum = Mojo::Util::sha1_hex($c->session->{csrf_token} . $user->login_password);
171 3 100       152 if ($checksum eq $val->param('digest')) {
172 2         65 $c->session(login_name => $user->login_name);
173 2         58 $c->user($user);
174 2         6 $c->app->log->info('$user ' . $user->login_name . ' logged in!');
175 2         108 delete $c->session->{csrf_token};
176 2         25 return 1;
177             }
178              
179 1 50       18 $c->debug('We should not be here! - wrong password') if $Ado::Control::DEV_MODE;
180 1         20 delete $c->session->{csrf_token};
181 1         20 return '';
182             }
183              
184             #used as helper within login()
185             # this method is called as return_url after the user
186             # agrees or denies access for the application
187             sub _login_google {
188 0     0     my ($c) = @_;
189 0           state $app = $c->app;
190 0           my $provider = $c->param('auth_method');
191 0           my $providers = $app->config('Ado::Plugin::Auth')->{providers};
192              
193             #second call should get the token it self
194 0           my $response = $c->oauth2->get_token($provider, $providers->{$provider});
195 0 0         do {
196 0           $c->debug("in _login_google \$response: " . $c->dumper($response));
197 0   0       $c->debug("in _login_google error from provider: " . ($c->param('error') || 'no error'));
198             } if $Ado::Control::DEV_MODE;
199 0 0         if ($response->{access_token}) { #Athenticate, create and login the user.
200             return _create_or_authenticate_google_user(
201             $c,
202             $response->{access_token},
203 0           $providers->{$provider}
204             );
205             }
206             else {
207             #Redirect to front-page and say sorry
208             # We are very sorry but we need to know you are a reasonable human being.
209 0   0       $c->flash(error_login => $c->l('oauth2_sorry[_1]', ucfirst($provider))
210             . ($c->param('error') || ''));
211 0           $c->app->log->error('error_response:' . $c->dumper($response));
212 0           $c->res->code(307); #307 Temporary Redirect
213 0           $c->redirect_to('/');
214             }
215 0           return;
216             }
217              
218             #used as helper within login()
219             # this method is called as return_url after the user
220             # agrees or denies access for the application
221             sub _login_facebook {
222 0     0     my ($c) = @_;
223 0           state $app = $c->app;
224 0           my $provider = $c->param('auth_method');
225 0           my $providers = $app->config('Ado::Plugin::Auth')->{providers};
226              
227             #second call should get the token it self
228 0           my $response = $c->oauth2->get_token($provider, $providers->{$provider});
229 0 0         do {
230 0           $c->debug("in _login_facebook \$response: " . $c->dumper($response));
231 0   0       $c->debug(
232             "in _login_facebook error from provider: " . ($c->param('error') || 'no error'));
233             } if $Ado::Control::DEV_MODE;
234 0 0         if ($response->{access_token}) { #Athenticate, create and login the user.
235             return _create_or_authenticate_facebook_user(
236             $c,
237             $response->{access_token},
238 0           $providers->{$provider}
239             );
240             }
241             else {
242             #Redirect to front-page and say sorry
243             # We are very sorry but we need to know you are a reasonable human being.
244 0   0       $c->flash(error_login => $c->l('oauth2_sorry[_1]', ucfirst($provider))
245             . ($c->param('error') || ''));
246 0           $c->app->log->error('error_response:' . $c->dumper($response));
247 0           $c->res->code(307); #307 Temporary Redirect
248 0           $c->redirect_to('/');
249             }
250 0           return;
251              
252             }
253              
254             sub _authenticate_oauth2_user {
255 0     0     my ($c, $user, $time) = @_;
256 0 0 0       if ( $user->disabled
      0        
      0        
257             || ($user->stop_date != 0 && $user->stop_date < $time)
258             || $user->start_date > $time)
259             {
260 0           $c->flash(login_message => $c->l('oauth2_disabled'));
261 0           $c->redirect_to('/');
262 0           return;
263             }
264 0           $c->session(login_name => $user->login_name);
265 0           $c->user($user);
266 0           $c->app->log->info('$user ' . $user->login_name . ' logged in!');
267 0           return 1;
268             }
269              
270             #Creates a user using given info from provider
271             sub _create_oauth2_user {
272 0     0     my ($c, $user_info, $provider) = @_;
273 0           state $app = $c->app;
274 0 0         if (my $user = Ado::Model::Users->add(_user_info_to_args($user_info, $provider))) {
275 0           $app->plugins->emit_hook(after_user_add => $c, $user, $user_info);
276 0           $c->user($user);
277 0           $c->session(login_name => $user->login_name);
278 0           $app->log->info($user->description . ' New $user ' . $user->login_name . ' logged in!');
279 0           $c->flash(login_message => $c->l('oauth2_wellcome[_1]', $user->name));
280 0           $c->redirect_to('/');
281 0           return 1;
282             }
283 0           $app->log->error($@);
284 0           return;
285             }
286              
287             #next two methods
288             #(_create_or_authenticate_facebook_user and _create_or_authenticate_google_user)
289             # exist only because we pass different parameters in the form
290             # which are specific to the provider.
291             # TODO: think of a way to map the generation of the form arguments to the
292             # specific provider so we can dramatically reduce the number of provider
293             # specific subroutines
294             sub _create_or_authenticate_facebook_user {
295 0     0     my ($c, $access_token, $provider) = @_;
296 0           my $ua = Mojo::UserAgent->new;
297 0           my $appsecret_proof = Digest::SHA::hmac_sha256_hex($access_token, $provider->{secret});
298 0           $c->debug('$appsecret_proof:' . $appsecret_proof);
299             my $user_info =
300             $ua->get($provider->{info_url},
301 0           form => {access_token => $access_token, appsecret_proof => $appsecret_proof})->res->json;
302 0 0         $c->debug('Response from info_url:' . $c->dumper($user_info)) if $Ado::Control::DEV_MODE;
303              
304 0           my $user = Ado::Model::Users->by_email($user_info->{email});
305 0           my $time = time;
306              
307 0 0         if ($user->id) {
308 0           return _authenticate_oauth2_user($c, $user, $time);
309             }
310              
311             #else create the user
312 0           return _create_oauth2_user($c, $user_info, $provider);
313             }
314              
315             sub _create_or_authenticate_google_user {
316 0     0     my ($c, $access_token, $provider) = @_;
317              
318             #make request for the user info
319 0           my $token_type = 'Bearer';
320 0           my $ua = Mojo::UserAgent->new;
321             my $user_info =
322 0           $ua->get($provider->{info_url} => {Authorization => "$token_type $access_token"})
323             ->res->json;
324              
325 0           my $user = Ado::Model::Users->by_email($user_info->{email});
326 0           my $time = time;
327              
328 0 0         if ($user->id) {
329 0           return _authenticate_oauth2_user($c, $user, $time);
330             }
331              
332             #else create the user
333 0           return _create_oauth2_user($c, $user_info, $provider);
334             }
335              
336             # Redirects to Consent screen
337             sub authorize {
338 0     0 1   my ($c) = @_;
339 0           my $m = $c->param('auth_method');
340 0           my $params = $c->app->config('Ado::Plugin::Auth')->{providers}{$m};
341 0           $params->{redirect_uri} = '' . $c->url_for("/login/$m")->to_abs;
342              
343             #This call will redirect the user to the provider Consent screen.
344 0           $c->redirect_to($c->oauth2->auth_url($m, %$params));
345 0           return;
346             }
347              
348             # Maps user info given from provider to arguments for
349             # Ado::Model::Users->new
350             sub _user_info_to_args {
351 0     0     my ($ui, $provider) = @_;
352 0           my %args;
353 0 0         if (index($provider->{info_url}, 'google') > -1) {
    0          
354 0           $args{first_name} = $ui->{given_name};
355 0           $args{last_name} = $ui->{family_name};
356             }
357             elsif (index($provider->{info_url}, 'facebook') > -1) {
358 0           $args{first_name} = $ui->{first_name};
359 0           $args{last_name} = $ui->{last_name};
360             }
361              
362             #Add another elsif to map different %args to $ui from a new provider
363             else {
364 0           Carp::croak('Unknown provider info_url:' . $provider->{info_url});
365             }
366 0           $args{email} = $ui->{email};
367 0           $args{login_name} = $ui->{email};
368 0           $args{login_name} =~ s/[\@\.]+//g;
369             $args{login_password} =
370 0           Mojo::Util::sha1_hex($args{login_name} . Ado::Sessions->generate_id());
371 0           $args{description} = "Registered via $provider->{info_url}!";
372 0           $args{created_by} = $args{changed_by} = 1;
373 0           $args{start_date} = $args{disabled} = $args{stop_date} = 0;
374              
375 0           return %args;
376             }
377             1;
378              
379              
380             =pod
381              
382             =encoding utf8
383              
384             =head1 NAME
385              
386             Ado::Plugin::Auth - Passwordless user authentication for Ado
387              
388             =head1 SYNOPSIS
389              
390             #in etc/ado.$mode.conf
391             plugins =>[
392             #...
393             'auth',
394             #...
395             ],
396              
397             #in etc/plugins/auth.$mode.conf
398             {
399             #methods which will be displayed in the "Sign in" menu
400             auth_methods => ['ado', 'facebook', 'google'],
401              
402             providers => {
403             google => {
404             key =>'123456789....apps.googleusercontent.com',
405             secret =>'YourSECR3T',
406             scope=>'profile email',
407             info_url => 'https://www.googleapis.com/userinfo/v2/me',
408             },
409             facebook => {
410             key =>'123456789',
411             secret =>'123456789abcdef',
412             scope =>'public_profile,email',
413             info_url => 'https://graph.facebook.com/v2.2/me',
414             },
415             }
416             }
417              
418             =head1 DESCRIPTION
419              
420             L is a plugin that authenticates users to an L system.
421             Users can be authenticated via Google, Facebook, locally and in the future
422             other authentication service-providers.
423              
424             B. When using the
425             local authentication method (ado) a digest is prepared in the browser using
426             JavaScript. The digest is sent and compared on the server side. The digest is
427             different in every POST request. The other authentication methods use the
428             services provided by well known service providers like Google, Facebook etc.
429             To use external authentication providers the module
430             L needs to be installed.
431              
432             =head1 CONFIGURATION
433              
434             The following options can be set in C. You can
435             find default options in C.
436              
437             =head2 auth_methods
438              
439             This option will enable the listed methods (services) which will be used to
440             authenticate a user. The services will be listed in the specified order in the
441             partial template C that can be included in any other template
442             on your site.
443              
444             #in etc/plugins/auth.$mode.conf
445             {
446             #methods which will be displayed in the "Sign in" menu
447             auth_methods => ['ado', 'google'],
448             }
449              
450             =head2 providers
451              
452             A Hash reference with keys representing names of providers (same as
453             auth_methods) and values, containing the configurations for the specific
454             providers. This option will be merged with already defined providers by
455             L. Add the rest of the needed configuration
456             options to auth.development.conf or auth.production.conf only because this is
457             highly sensitive and application specific information.
458              
459             #Example for google:
460             google =>{
461             #client_id
462             key =>'123456654321abcd.apps.googleusercontent.com',
463             secret =>'Y0uRS3cretHEre',
464             scope=>'profile email',
465             info_url => 'https://www.googleapis.com/userinfo/v2/me',
466             },
467              
468             =head2 routes
469              
470             Currently defined routes are described in L.
471              
472             =head1 CONDITIONS
473              
474             L provides the following conditions to be used by routes.
475             To find more about conditions read L.
476              
477             =head2 authenticated
478              
479             Condition for routes used to check if a user is authenticated.
480              
481             =cut
482              
483             #TODO:?
484             #Additional parameters can be passed to specify the preferred
485             #authentication method to be preselected in the login form
486             #if condition redirects to C.
487              
488             =pod
489              
490             # add the condition programatically
491             $app->routes->route('/ado-users/:action', over => {authenticated=>1});
492             $app->routes->route('/ado-users/:action',
493             over => [authenticated => 1, ingroup => 'admin']
494             );
495              
496             #in etc/ado.$mode.conf or etc/plugins/foo.$mode.conf
497             routes => [
498             #...
499             {
500             route => '/ado-users/:action:id',
501             via => [qw(PUT DELETE)],
502              
503             # only authenticated users can edit and delete users,
504             # and only if they are authorized to do so
505             over => [authenticated => 1, ingroup => 'admin'],
506             to =>'ado-users#edit'
507             }
508             ],
509              
510             =head2 ingroup
511              
512             Checks if a user is in the given group. Returns true or false.
513              
514             # in etc/plugins/routes.conf or etc/plugins/foo.conf
515             {
516             route => '/vest',
517             via => ['GET'],
518             to => 'vest#screen',
519             over => [authenticated => 1, ingroup => 'foo'],
520             }
521             # programatically
522             $app->routes->route('/ado-users/:action', over => {ingroup => 'foo'});
523              
524             =head1 HELPERS
525              
526             L provides the following helpers for use in
527             L methods and templates.
528              
529             =head2 login_ado
530              
531             Finds and logs in a user locally. Returns true on success, false otherwise.
532              
533             =head2 login_google
534              
535             Called via C. Finds an existing user and logs it in via Google.
536             Creates a new user if it does not exist and logs it in via Google. The new
537             user can login via any supported OAuth2 provider as long as it has the same
538             email. The user can not login using Ado local authentication because he does
539             not know his password, which is randomly generated. Returns true on success,
540             false otherwise.
541              
542             =head2 login_facebook
543              
544             Called via C. Finds an existing user and logs it in via
545             Facebook. Creates a new user if it does not exist and logs it in via Facebook.
546             The new user can login via any supported Oauth2 provider as long as it has the
547             same email. The user can not login using Ado local authentication because he
548             does not know his password, which is randomly generated. Returns true on
549             success, false otherwise.
550              
551             =head1 HOOKS
552              
553             Ado::Plugin::Auth emits the following hooks.
554              
555             =head2 after_login
556              
557             In your plugin you can define some functionality to be executed right after a
558             user has logged in. For example add some links to the adobar template,
559             available only to logged-in users. Only the controller C<$c> is passed to this
560             hook.
561              
562             #example from Ado::Plugin::Admin
563             $app->hook(
564             after_login => sub {
565             push @{shift->session->{adobar_links} //= []},
566             {icon => 'dashboard', href => '/ado', text => 'Dashboard'};
567             }
568             );
569              
570              
571             =head2 after_user_add
572              
573             $app->hook(after_user_add => sub {
574             my ($c, $user, $raw_data) = @_;
575             my $group = $user->add_to_group(ingroup=>'vest');
576             ...
577             });
578              
579             In your plugin you can define some functionality to be executed right after a
580             user is added. For example add a user to a group after registration. Passed
581             the controller, the newly created C<$user> and the $raw_data used to create
582             the user.
583              
584             =head1 ROUTES
585              
586             L provides the following routes (actions):
587              
588             =head2 /authorize/:auth_method
589              
590             Redirects to an OAuth2 provider consent screen where the user can authorize
591             L to use his information or not. Currently L supports Facebook and
592             Google.
593              
594             =head2 /login
595              
596             /login/ado
597              
598             If accessed using a C request displays a login form. If accessed via
599             C performs authentication using C system database, and emits the
600             hook L.
601              
602             /login/facebook
603              
604             Facebook consent screen redirects to this action. This action is handled by
605             L.
606              
607              
608             /login/google
609              
610             Google consent screen redirects to this action. This action is handled by
611             L.
612              
613              
614             =head2 /logout
615              
616             Expires the session and redirects to the base URL.
617              
618             $c->logout();
619              
620             =head1 TEMPLATES
621              
622             L uses the following templates. The paths are in the
623             C folder. Feel free to move them to the site_templates folder and
624             modify them for your needs.
625              
626             =head2 partials/authbar.html.ep
627              
628             Renders a menu dropdown for choosing methods for signing in.
629              
630             =head2 partials/login_form.html.ep
631              
632             Renders a Login form to authenticate locally.
633              
634             =head2 login.html.ep
635              
636             Renders a page containing the login form above.
637              
638             =head1 METHODS
639              
640             L inherits all methods from L and implements
641             the following new ones.
642              
643              
644             =head2 register
645              
646             This method is called by C<$app-Eplugin>. Registers the plugin in L
647             application and merges authentication configuration from
648             C<$MOJO_HOME/etc/ado.conf> with settings defined in
649             C<$MOJO_HOME/etc/plugins/auth.conf>. Authentication settings defined in
650             C will override those defined in
651             C. Authentication settings defined in C will
652             override both.
653              
654             =head1 TODO
655              
656             The following authentication methods are in the TODO list: linkedin, github.
657             Others may be added later. Please help by implementing authentication via more
658             providers.
659              
660             =head1 SEE ALSO
661              
662             L,
663             L, L, L,
664             L, L
665              
666             =head1 AUTHOR
667              
668             Красимир Беров (Krasimir Berov)
669              
670             =head1 COPYRIGHT AND LICENSE
671              
672             Copyright 2014-2016 Красимир Беров (Krasimir Berov).
673              
674             This program is free software, you can redistribute it and/or modify it under
675             the terms of the GNU Lesser General Public License v3 (LGPL-3.0). You may
676             copy, distribute and modify the software provided that modifications are open
677             source. However, software that includes the license may release under a
678             different license.
679              
680             See http://opensource.org/licenses/lgpl-3.0.html for more information.
681              
682             =cut