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