File Coverage

blib/lib/Dancer2/Plugin/Auth/Extensible.pm
Criterion Covered Total %
statement 469 515 91.0
branch 190 228 84.2
condition 72 98 83.6
subroutine 63 69 91.3
pod 17 19 89.4
total 811 929 88.5


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::Auth::Extensible;
2              
3             our $VERSION = '0.709';
4              
5 13     13   6570309 use strict;
  13         101  
  13         375  
6 13     13   66 use warnings;
  13         26  
  13         302  
7 13     13   62 use Carp;
  13         23  
  13         854  
8 13     13   494 use Dancer2::Core::Types qw(ArrayRef Bool HashRef Int Str);
  13         121343  
  13         126  
9 13     13   22140 use Dancer2::FileUtils qw(path);
  13         730  
  13         672  
10 13     13   6039 use Dancer2::Template::Tiny;
  13         710453  
  13         493  
11 13     13   6188 use File::Share qw(dist_dir);
  13         178661  
  13         761  
12 13     13   9771 use HTTP::BrowserDetect;
  13         225406  
  13         557  
13 13     13   119 use List::Util qw(first);
  13         30  
  13         962  
14 13     13   93 use Module::Runtime qw(use_module);
  13         26  
  13         126  
15 13     13   638 use Scalar::Util qw(blessed);
  13         27  
  13         564  
16 13     13   7232 use Session::Token;
  13         26841  
  13         452  
17 13     13   2703 use Try::Tiny;
  13         6742  
  13         694  
18 13     13   463 use URI::Escape;
  13         1189  
  13         625  
19 13     13   546 use URI;
  13         2302  
  13         308  
20 13     13   5731 use URI::QueryParam; # Needed to access query_form_hash(), although may be loaded anyway
  13         9845  
  13         399  
21 13     13   6721 use Dancer2::Plugin;
  13         174201  
  13         114  
22              
23             #
24             # config attributes
25             #
26              
27             has denied_page => (
28             is => 'ro',
29             isa => Str,
30             from_config => sub { '/login/denied' },
31             );
32              
33             has disable_roles => (
34             is => 'ro',
35             isa => Bool,
36             from_config => sub { 0 },
37             );
38              
39             has exit_page => (
40             is => 'ro',
41             isa => Str,
42             from_config => sub { '/' },
43             );
44              
45             has login_page => (
46             is => 'ro',
47             isa => Str,
48             from_config => sub { '/login' },
49             );
50              
51             has login_template => (
52             is => 'ro',
53             isa => Str,
54             from_config => sub { 'login' },
55             );
56              
57             has login_page_handler => (
58             is => 'ro',
59             isa => Str,
60             from_config => sub { '_default_login_page' },
61             );
62              
63             has login_without_redirect => (
64             is => 'ro',
65             isa => Bool,
66             from_config => sub { 0 },
67             );
68              
69             has logout_page => (
70             is => 'ro',
71             isa => Str,
72             from_config => sub { '/logout' },
73             );
74              
75             has no_login_handler => (
76             is => 'ro',
77             isa => Bool,
78             from_config => 1,
79             default => sub { 0 },
80             );
81              
82             has mailer => (
83             is => 'ro',
84             isa => HashRef,
85             from_config => sub { '' },
86             );
87              
88             has mail_from => (
89             is => 'ro',
90             isa => Str,
91             from_config => sub { '' },
92             );
93              
94             has no_default_pages => (
95             is => 'ro',
96             isa => Bool,
97             from_config => sub { 0 },
98             );
99              
100             has password_generator => (
101             is => 'ro',
102             isa => Str,
103             from_config => sub { '_default_password_generator' },
104             );
105              
106             has password_reset_send_email => (
107             is => 'ro',
108             isa => Str,
109             from_config => sub { '_default_email_password_reset' },
110             );
111              
112             has password_reset_text => (
113             is => 'ro',
114             isa => Str,
115             from_config => sub { '' },
116             );
117              
118             has permission_denied_handler => (
119             is => 'ro',
120             isa => Str,
121             from_config => sub { '_default_permission_denied_handler' },
122             );
123              
124             has permission_denied_page_handler => (
125             is => 'ro',
126             isa => Str,
127             from_config => sub { '_default_permission_denied_page' },
128             );
129              
130             has realms => (
131             is => 'ro',
132             isa => ArrayRef,
133             default => sub {
134             my @realms;
135             while ( my ( $name, $realm ) = each %{ $_[0]->config->{realms} } ) {
136             $realm->{priority} ||= 0;
137             push @realms, { name => $name, %$realm };
138             };
139             return [ sort { $b->{priority} <=> $a->{priority} } @realms ];
140             },
141             );
142              
143             has realm_names => (
144             is => 'lazy',
145             isa => ArrayRef,
146             default => sub {
147             return [ map { $_->{name} } @{ $_[0]->realms } ];
148             },
149             );
150              
151             has realm_count => (
152             is => 'lazy',
153             isa => Int,
154             default => sub { return scalar @{ $_[0]->realms } },
155             );
156              
157             # return realm config hash reference by name
158             sub realm {
159 40     40 0 103 my ( $self, $name ) = @_;
160 40 100       533 croak "realm name not provided" unless $name;
161 36     70   170 my $realm = first { $_->{name} eq $name } @{ $self->realms };
  70         162  
  36         181  
162 36         449 return $realm;
163             }
164              
165             has record_lastlogin => (
166             is => 'ro',
167             isa => Bool,
168             from_config => sub { 0 },
169             );
170              
171             has reset_password_handler => (
172             is => 'ro',
173             isa => Bool,
174             from_config => sub { 0 },
175             plugin_keyword => 1,
176             );
177              
178             has user_home_page => (
179             is => 'ro',
180             isa => Str,
181             from_config => sub { '/' },
182             );
183              
184             has welcome_send => (
185             is => 'ro',
186             isa => Str,
187             from_config => sub { '_default_welcome_send' },
188             );
189              
190             has welcome_text => (
191             is => 'ro',
192             isa => Str,
193             from_config => sub { '' },
194             );
195              
196             #
197             # other attributes
198             #
199              
200             has realm_providers => (
201             is => 'ro',
202             isa => HashRef,
203             default => sub { {} },
204             init_arg => undef,
205             );
206              
207             has _template_tiny => (
208             is => 'ro',
209             default => sub { Dancer2::Template::Tiny->new },
210             );
211              
212             #
213             # hooks
214             #
215              
216             plugin_hooks 'before_authenticate_user', 'after_authenticate_user',
217             'before_create_user', 'after_create_user', 'after_reset_code_success',
218             'login_required', 'permission_denied', 'after_login_success',
219             'before_logout';
220              
221             #
222             # keywords
223             #
224              
225             plugin_keywords 'authenticate_user', 'create_user', 'get_user_details',
226             'logged_in_user', 'logged_in_user_lastlogin',
227             'logged_in_user_password_expired', 'password_reset_send',
228             [ 'require_all_roles', 'requires_all_roles' ],
229             [ 'require_any_role', 'requires_any_role' ],
230             [ 'require_login', 'requires_login' ],
231             [ 'require_role', 'requires_role' ],
232             'update_current_user', 'update_user', 'user_has_role', 'user_password',
233             'user_roles';
234              
235             #
236             # public methods
237             #
238              
239             sub BUILD {
240 15     15 0 1435 my $plugin = shift;
241 15         61 my $app = $plugin->app;
242              
243 15         108 Scalar::Util::weaken( my $weak_plugin = $plugin );
244              
245 15 100       264 warn "No Auth::Extensible realms configured with which to authenticate user"
246             unless $plugin->realm_count;
247              
248             # Force all providers to load whilst we have access to the full dsl.
249             # If we try and load later, then if the provider is using other
250             # keywords (such as schema) they will not be available from the dsl.
251 15         435 for my $realm ( @{ $plugin->realm_names } ) {
  15         279  
252 34         17184 $plugin->auth_provider( $realm );
253             }
254              
255 14 100       11489 if ( !$plugin->no_default_pages ) {
256              
257 13         635 my $login_page = $plugin->login_page;
258 13         572 my $denied_page = $plugin->denied_page;
259              
260             # Match optional reset code, but not "denied"
261             $app->add_route(
262             method => 'get',
263             regexp => qr!^$login_page/?([\w]{32})?$!,
264             code => sub {
265 20     20   224660 my $app = shift;
266              
267 20 100       100 if ( $weak_plugin->logged_in_user ) {
268             # User is already logged in so redirect elsewhere
269             # uncoverable condition false
270 4   66     14 $app->redirect(
271             _return_url($app) || $weak_plugin->user_home_page );
272             }
273              
274             # Reset password code submitted?
275 16         87 my ($code) = $app->request->splat;
276              
277 16 100 100     358 if ( $code
      100        
278             && $weak_plugin->reset_password_handler
279             && $weak_plugin->user_password( code => $code ) )
280             {
281 1         7 $app->request->parameters->set('password_code_valid' => 1),
282             }
283              
284 13     13   88840 no strict 'refs';
  13         37  
  13         1524  
285 16         215 return &{ $weak_plugin->login_page_handler }($weak_plugin);
  16         334  
286             },
287 13         898 );
288              
289             $app->add_route(
290             method => 'get',
291             regexp => qr!^$denied_page$!,
292             code => sub {
293 2     2   4485 my $app = shift;
294 2         72 $app->response->status(403);
295 13     13   107 no strict 'refs';
  13         149  
  13         15438  
296 2         288 return &{ $weak_plugin->permission_denied_page_handler }($weak_plugin);
  2         49  
297             },
298 13         52602 );
299             }
300              
301 14 100       4117 if ( !$plugin->no_login_handler ) {
302              
303 13         560 my $login_page = $plugin->login_page;
304 13         338 my $logout_page = $plugin->logout_page;
305              
306             # Match optional reset code, but not "denied"
307 13         813 $app->add_route(
308             method => 'post',
309             regexp => qr!^$login_page/?([\w]{32})?$!,
310             code => \&_post_login_route,
311             );
312              
313 13         8066 for my $method (qw/get post/) {
314 26         4122 $app->add_route(
315             method => $method,
316             regexp => qr!^$logout_page$!,
317             code => \&_logout_route,
318             );
319             }
320             }
321              
322 14 100       4021 if ( $plugin->login_without_redirect ) {
323              
324             # Add a post route so we can catch transparent login.
325             # This is a little sucky but since no hooks are called before
326             # route dispatch then adding this wildcard route now does at
327             # least make sure it gets added before any routes that use this
328             # plugin's route decorators are added.
329              
330             $plugin->app->add_route(
331             method => 'post',
332             regexp => qr/.*/,
333             code => sub {
334 9     9   77044 my $app = shift;
335 9         23 my $request = $app->request;
336              
337             # See if this is actually a POST login.
338 9         28 my $username = $request->body_parameters->get(
339             '__auth_extensible_username');
340              
341 9         82 my $password = $request->body_parameters->get(
342             '__auth_extensible_password');
343              
344 9 100 66     89 if ( defined $username && defined $password ) {
345              
346 6         14 my $auth_realm = $request->body_parameters->get(
347             '__auth_extensible_realm');
348              
349             # Remove the auth params since the forward we call later
350             # will cause dispatch to retry this route again if
351             # the original route was a post since dispatch starts
352             # again from the start of the route list and this
353             # wildcard route will get hit again causing a loop.
354 6         49 foreach (qw/username password realm/) {
355 18         453 $request->body_parameters->remove(
356             "__auth_extensible_$_");
357             }
358              
359             # Stash method and params since we delete these from
360             # the session if login is successful but we still need
361             # them for the forward to the original route after
362             # success.
363 6         202 my $method =
364             $app->session->read('__auth_extensible_method');
365 6         4775 my $params =
366             $app->session->read('__auth_extensible_params');
367              
368             # Attempt authentication.
369 6         172 my ( $success, $realm ) =
370             $weak_plugin->authenticate_user( $username,
371             $password, $auth_realm );
372              
373 6 100       17 if ($success) {
374 3         43 $app->session->delete('__auth_extensible_params');
375 3         220 $app->session->delete('__auth_extensible_method');
376              
377             # Change session ID if we have a new enough D2
378             # version with support.
379 3 50       171 $app->change_session_id
380             if $app->can('change_session_id');
381              
382 3         871 $app->session->write( logged_in_user => $username );
383 3         198 $app->session->write( logged_in_user_realm => $realm );
384 3         211 $app->log( core => "Realm is $realm" );
385 3         171 $weak_plugin->execute_plugin_hook(
386             'after_login_success');
387              
388             }
389             else {
390 3         14 $app->request->var( login_failed => 1 );
391             }
392             # Now forward to the original route using method and
393             # params stashed in the session.
394 6         582 $app->forward(
395             $request->path,
396             $params,
397             { method => $method }
398             );
399             }
400 3         9 $app->pass;
401             },
402 1         81 );
403             }
404             }
405              
406             sub auth_provider {
407 450     450 1 1491 my ( $plugin, $realm ) = @_;
408              
409             # If no realm was provided, but we have a logged in user, use their realm.
410             # Don't try and read the session any earlier though, as it won't be
411             # available on plugin load
412 450 100       1419 if ( !defined $realm ) {
413 33 100       576 if ( $plugin->app->session->read('logged_in_user') ) {
414 31         6850 $realm = $plugin->app->session->read('logged_in_user_realm');
415             }
416             else {
417 2         1746 croak "auth_provider needs realm or there must be a logged in user";
418             }
419             }
420              
421             # First, if we already have a provider for this realm, go ahead and use it:
422             return $plugin->realm_providers->{$realm}
423 448 100       3290 if exists $plugin->realm_providers->{$realm};
424              
425             # OK, we need to find out what provider this realm uses, and get an instance
426             # of that provider, configured with the settings from the realm.
427 36 100       116 my $realm_settings = $plugin->realm($realm)
428             or croak "Invalid realm $realm";
429              
430             my $provider_class = $realm_settings->{provider}
431 34 100       382 or croak "No provider configured - consult documentation for "
432             . __PACKAGE__;
433              
434 33 100       115 if ( $provider_class !~ /::/ ) {
435 24         129 $provider_class = __PACKAGE__ . "::Provider::$provider_class";
436             }
437              
438 33         138 return $plugin->realm_providers->{$realm} =
439             use_module($provider_class)->new(
440             plugin => $plugin,
441             %$realm_settings,
442             );
443             }
444              
445             sub authenticate_user {
446 130     130 1 65433 my ( $plugin, $username, $password, $realm ) = @_;
447 130         345 my ( @errors, $success, $auth_realm );
448              
449 130         3252 $plugin->execute_plugin_hook( 'before_authenticate_user',
450             { username => $username, password => $password, realm => $realm } );
451              
452             # username and password must be simple non-empty scalars
453 130 100 100     117272 if ( defined $username
      100        
      100        
      100        
      100        
454             && ref($username) eq ''
455             && $username ne ''
456             && defined $password
457             && ref($password) eq ''
458             && $password ne '' )
459             {
460 67 100       293 my @realms_to_check = $realm ? ($realm) : @{ $plugin->realm_names };
  53         1140  
461              
462 67         768 for my $realm (@realms_to_check) {
463 161         1190 $plugin->app->log( debug =>
464             "Attempting to authenticate $username against realm $realm" );
465 161         85947 my $provider = $plugin->auth_provider($realm);
466              
467 161 100       2887 my %lastlogin =
468             $plugin->record_lastlogin
469             ? ( lastlogin => 'logged_in_user_lastlogin' )
470             : ();
471              
472             eval {
473 161         2397 $success =
474             $provider->authenticate_user( $username, $password,
475             %lastlogin );
476 160         29874 1;
477 161 100       1892 } or do {
478             # uncoverable condition right
479 1   50     273 my $err = $@ || "Unknown error";
480 1         10 $plugin->app->log(
481             error => "$realm provider threw error: $err" );
482 1         559 push @errors, $err;
483             };
484 161 100       526 if ($success) {
485 42         340 $plugin->app->log( debug => "$realm accepted user $username" );
486 42         21792 $auth_realm = $realm;
487 42         174 last;
488             }
489             }
490             }
491              
492             # force 0 or 1 for success
493 130         399 $success = 0+!!$success;
494              
495 130         3238 $plugin->execute_plugin_hook(
496             'after_authenticate_user',
497             {
498             username => $username,
499             password => $password,
500             realm => $auth_realm,
501             errors => \@errors,
502             success => $success,
503             }
504             );
505              
506 130 100       69098 return wantarray ? ( $success, $auth_realm ) : $success;
507             }
508              
509             sub create_user {
510 12     12 1 120940 my $plugin = shift;
511 12         60 my %options = @_;
512 12         29 my ( $user, @errors );
513              
514             croak "Realm must be specified when more than one realm configured"
515 12 100 100     167 if !$options{realm} && $plugin->realm_count > 1;
516              
517 11         339 $plugin->execute_plugin_hook( 'before_create_user', \%options );
518              
519             # uncoverable condition false
520 11   66     55403 my $realm = delete $options{realm} || $plugin->realm_names->[0];
521 11         65 my $email_welcome = delete $options{email_welcome};
522 11         33 my $password = delete $options{password};
523 11         56 my $provider = $plugin->auth_provider($realm);
524              
525 11 100       33 eval { $user = $provider->create_user(%options); 1; } or do {
  11         95  
  9         47  
526             # uncoverable condition right
527 2   50     606 my $err = $@ || "Unknown error";
528 2         21 $plugin->app->log( error => "$realm provider threw error: $err" );
529 2         1130 push @errors, $err;
530             };
531              
532 11 100       51 if ($user) {
533             # user creation successful
534 9 100       45 if ($email_welcome) {
    100          
535 1         5 my $code = _reset_code();
536              
537             # Would be slightly more efficient to do this at time of creation,
538             # but this keeps the code simpler for the provider
539             $provider->set_user_details( $options{username},
540 1         262 pw_reset_code => $code );
541              
542             # email hard-coded as per password_reset_send()
543             my %params =
544 1         5 ( code => $code, email => $options{email}, user => $user );
545              
546 13     13   118 no strict 'refs';
  13         43  
  13         11056  
547 1         4 &{ $plugin->welcome_send }( $plugin, %params );
  1         27  
548             }
549             elsif ($password) {
550             eval {
551 6         36 $provider->set_user_password( $options{username}, $password );
552 5         24 1;
553 6 100       16 } or do {
554             # uncoverable condition right
555 1   50     156 my $err = $@ || "Unknown error";
556 1         10 $plugin->app->log(
557             error => "$realm provider threw error: $err" );
558 1         574 push @errors, $err;
559             };
560             }
561             }
562              
563             $plugin->execute_plugin_hook( 'after_create_user', $options{username},
564 11         292 $user, \@errors );
565              
566 11         5209 return $user;
567             }
568              
569             sub get_user_details {
570 85     85 1 2940 my ( $plugin, $username, $realm ) = @_;
571 85         183 my $user;
572 85 100       277 return unless defined $username;
573              
574 83 100       273 my @realms_to_check = $realm ? ($realm) : @{ $plugin->realm_names };
  5         124  
575              
576 83         287 for my $realm (@realms_to_check) {
577 91         653 $plugin->app->log(
578             debug => "Attempting to find user $username in realm $realm" );
579 91         51308 my $provider = $plugin->auth_provider($realm);
580 91 100       234 eval { $user = $provider->get_user_details($username); 1; } or do {
  91         455  
  90         326  
581             # uncoverable condition right
582 1   50     154 my $err = $@ || "Unknown error";
583 1         8 $plugin->app->log( error => "$realm provider threw error: $err" );
584             };
585 91 100       913 last if $user;
586             }
587 83         281 return $user;
588             }
589              
590             sub logged_in_user {
591 162     162 1 24534 my $plugin = shift;
592 162         544 my $app = $plugin->app;
593 162         2892 my $session = $app->session;
594 162         168733 my $request = $app->request;
595              
596 162 100       642 if ( my $username = $session->read('logged_in_user') ) {
597 71         2141 my $existing = $request->vars->{logged_in_user_hash};
598 71 100       396 return $existing if $existing;
599 67         205 my $realm = $session->read('logged_in_user_realm');
600 67         1730 my $provider = $plugin->auth_provider($realm);
601 67 100       523 my $user =
602             $provider->can('get_user_details')
603             ? $plugin->get_user_details( $username, $realm )
604             : +{ username => $username };
605 67         305 $request->vars->{logged_in_user_hash} = $user;
606 67         566 return $user;
607             }
608             else {
609 91         2846 return undef; # Ensure function doesn't cause problems in list context (GH92)
610             }
611             }
612              
613             sub logged_in_user_lastlogin {
614 2     2 1 54 my $lastlogin = shift->app->session->read('logged_in_user_lastlogin');
615             # We don't expect any bad $lastlogin values during testing so mark as many
616             # as possible as uncoverable.
617             # uncoverable branch false
618             # uncoverable condition right
619 2 50 66     1871 if ( defined $lastlogin && ref($lastlogin) eq '' && $lastlogin =~ /^\d+$/ )
      66        
620             {
621             # A sane epoch time. Old Provider::DBIC stores DateTime in the session
622             # which might get stringified or perhaps not and some session engines
623             # might fail to serialize/deserialize so we now store epoch and
624             # convert back to DateTime.
625 1         10 $lastlogin = DateTime->from_epoch( epoch => $lastlogin );
626             }
627 2         358 return $lastlogin;
628             }
629              
630             sub logged_in_user_password_expired {
631 3     3 1 23 my $plugin = shift;
632 3 100       10 return unless $plugin->logged_in_user;
633 2         7 my $provider = $plugin->auth_provider;
634 2         8 $provider->password_expired( $plugin->logged_in_user );
635             }
636              
637             sub password_reset_send {
638 2     2 1 9 my ( $plugin, %options ) = @_;
639              
640 2         6 my $result = 0;
641              
642             my @realms_to_check =
643             $options{realm}
644             ? ( $options{realm} )
645 2 50       9 : @{ $plugin->realm_names };
  2         44  
646              
647             my $username = $options{username}
648 2 50       31 or croak "username must be passed to password_reset_send";
649              
650 2         6 foreach my $realm (@realms_to_check) {
651 6         12 my $this_result;
652 6         48 $plugin->app->log( debug =>
653             "Attempting to find $username in realm $realm for password reset"
654             );
655 6         3389 my $provider = $plugin->auth_provider($realm);
656              
657             # Generate random string for the password reset URL
658 6         22 my $code = _reset_code();
659             my $user = try {
660 6     6   307 $provider->set_user_details( $username, pw_reset_code => $code );
661             }
662             catch {
663 0     0   0 $plugin->app->log(
664             debug => "Failed to set_user_details with $realm: $_" );
665 6         1325 };
666 6 100       225 if ($user) {
667 1         10 $plugin->app->log(
668             debug => "got one");
669              
670             # Okay, so email key is hard-coded, and therefore relies on the
671             # provider returning that key. The alternative is to have a
672             # separate provider function to get an email address, which seems
673             # an overkill. Providers can make the email key configurable if
674             # need be
675 1 50       533 my $email = blessed $user ? $user->email : $user->{email};
676 1         6 my %options = ( code => $code, email => $email );
677              
678 13     13   108 no strict 'refs';
  13         29  
  13         24193  
679             $result++
680 1 50       4 if &{ $plugin->password_reset_send_email }( $plugin, %options );
  1         21  
681             }
682             }
683 2 100       11 $result ? 1 : 0; # 1 if at least one send was successful
684             }
685              
686             sub require_all_roles {
687 5     5 1 252 my $plugin = shift;
688 5 100       104 croak "Cannot use require_all_roles since roles are disabled by disable_roles setting"
689             if $plugin->disable_roles;
690 4         45 return $plugin->_build_wrapper( @_, 'all' );
691             }
692              
693             sub require_any_role {
694 3     3 1 1392 my $plugin = shift;
695 3 100       68 croak "Cannot use require_any_role since roles are disabled by disable_roles setting"
696             if $plugin->disable_roles;
697 2         26 return $plugin->_build_wrapper( @_, 'any' );
698             }
699              
700              
701             sub require_login {
702 17     17 1 133480 my $plugin = shift;
703 17         30 my $coderef = shift;
704              
705             return sub {
706 63 100 100 63   392549 if ( !$coderef || ref $coderef ne 'CODE' ) {
707 4         27 $plugin->app->log(
708             warning => "Invalid require_login usage, please see docs" );
709             }
710              
711             # User already logged in so give them the page.
712 63 100       2579 return $coderef->($plugin)
713             if $plugin->logged_in_user;
714              
715 27         131 return $plugin->_check_for_login( $coderef );
716 17         155 };
717             }
718              
719             sub require_role {
720 9     9 1 1007 my $plugin = shift;
721 9 100       184 croak "Cannot use require_role since roles are disabled by disable_roles setting"
722             if $plugin->disable_roles;
723 8         149 return $plugin->_build_wrapper( @_, 'single' );
724             }
725              
726             sub update_current_user {
727 3     3 1 33 my ( $plugin, %update ) = @_;
728              
729 3         58 my $session = $plugin->app->session;
730 3 100       2407 if ( my $username = $session->read('logged_in_user') ) {
731 2         62 my $realm = $session->read('logged_in_user_realm');
732 2         56 $plugin->update_user( $username, realm => $realm, %update );
733             }
734             else {
735 1         35 $plugin->app->log( debug =>
736             "Could not update current user as no user currently logged in" );
737             }
738             }
739              
740             sub update_user {
741 9     9 1 7335 my ( $plugin, $username, %update ) = @_;
742              
743             croak "Realm must be specified when more than one realm configured"
744 9 100 100     96 if !$update{realm} && $plugin->realm_count > 1;
745              
746             # uncoverable condition false
747 8   66     73 my $realm = delete $update{realm} || $plugin->realm_names->[0];
748 8         47 my $provider = $plugin->auth_provider($realm);
749 8         71 my $updated = $provider->set_user_details( $username, %update );
750 8         208 my $cur_user = $plugin->app->session->read('logged_in_user');
751 8 100 100     15292 $plugin->app->request->vars->{logged_in_user_hash} = $updated
752             if $cur_user && $cur_user eq $username;
753 8         63 $updated;
754             }
755              
756             sub user_has_role {
757 15     15 1 167751 my $plugin = shift;
758 15 100       285 croak "Cannot call user_has_role since roles are disabled by disable_roles setting"
759             if $plugin->disable_roles;
760              
761 14         125 my ( $username, $want_role );
762 14 100       45 if ( @_ == 2 ) {
763 6         18 ( $username, $want_role ) = @_;
764             }
765             else {
766 8         143 $username = $plugin->app->session->read('logged_in_user');
767 8         247 $want_role = shift;
768             }
769              
770 14 100       46 return unless defined $username;
771              
772 12         37 my $roles = $plugin->user_roles($username);
773              
774 12         34 for my $has_role (@$roles) {
775 18 100       71 return 1 if $has_role eq $want_role;
776             }
777              
778 4         17 return 0;
779             }
780              
781             sub user_password {
782 16     16 1 527 my ( $plugin, %params ) = @_;
783              
784 16         50 my ( $username, $realm );
785              
786             my @realms_to_check =
787             $params{realm}
788             ? ( $params{realm} )
789 16 100       71 : @{ $plugin->realm_names };
  14         299  
790              
791             # Expect either a code, username or nothing (for logged-in user)
792 16 100       216 if ( exists $params{code} ) {
793 6 50       39 my $code = $params{code} or return;
794 6         24 foreach my $realm_check (@realms_to_check) {
795 18         6869 my $provider = $plugin->auth_provider($realm_check);
796              
797             # Realm may not support get_user_by_code
798             $username = try {
799 18     18   849 $provider->get_user_by_code($code);
800             }
801             catch {
802 0     0   0 $plugin->app->log( 'debug',
803             "Failed to check for code with $realm_check: $_" );
804 18         145 };
805 18 100       699 if ($username) {
806 3         35 $plugin->app->log( 'debug',
807             "Found $username for code with $realm_check" );
808 3         1548 $realm = $realm_check;
809 3         11 last;
810             }
811             else {
812 15         97 $plugin->app->log( 'debug',
813             "No user found in realm $realm_check with code $code" );
814             }
815             }
816 6 100       1682 return unless $username;
817             }
818             else {
819 10 100       39 if ( !$params{username} ) {
820 2 50       45 $username = $plugin->app->session->read('logged_in_user')
821             or croak "No username specified and no logged-in user";
822 2         2123 $realm = $plugin->app->session->read('logged_in_user_realm');
823             }
824             else {
825 8         20 $username = $params{username};
826 8         20 $realm = $params{realm};
827             }
828 10 100       93 if ( exists $params{password} ) {
829 8         17 my $success;
830              
831             # Possible that realm will not be set before this statement
832             ( $success, $realm ) =
833             $plugin->authenticate_user( $username, $params{password},
834 8         43 $realm );
835 8 100       55 $success or return;
836             }
837             }
838              
839             # We now have a valid user. Reset the password?
840 9 100       48 if ( my $new_password = $params{new_password} ) {
841 4 100       16 if ( !$realm ) {
842              
843             # It's possible that the realm is unknown at this stage
844 2         8 foreach my $realm_check (@realms_to_check) {
845 6         29 my $provider = $plugin->auth_provider($realm_check);
846 6 100       24 $realm = $realm_check if $provider->get_user_details($username);
847             }
848 2 50       10 return unless $realm; # Invalid user
849             }
850 4         26 my $provider = $plugin->auth_provider($realm);
851 4         24 $provider->set_user_password( $username, $new_password );
852 4 100       21 if ( $params{code} ) {
853              
854             # Stop reset code being reused
855 1         6 $provider->set_user_details( $username, pw_reset_code => undef );
856              
857             # Force them to login if this was a reset with a code. This forces
858             # a check that they have the new password correct, and there is a
859             # chance they could have been logged-in as another user
860 1         8 $plugin->app->destroy_session;
861             }
862             }
863 9         1758 $username;
864             }
865              
866             sub user_roles {
867 32     32 1 20557 my ( $plugin, $username, $realm ) = @_;
868 32 100       598 croak
869             "Cannot call user_roles since roles are disabled by disable_roles setting"
870             if $plugin->disable_roles;
871              
872 31 100       328 if ( !defined $username ) {
873             # assume logged_in_user so clear realm and look for user
874 15         32 $realm = undef;
875 15         259 $username = $plugin->app->session->read('logged_in_user');
876 15 100       2632 croak "user_roles needs a username or a logged in user"
877             unless $username;
878             }
879              
880 29         88 my $roles = $plugin->auth_provider($realm)->get_user_roles($username);
881 29 50       86 return unless defined $roles;
882 29 100       130 return wantarray ? @$roles : $roles;
883             }
884              
885             #
886             # private methods
887             #
888              
889             sub _build_wrapper {
890 14     14   33 my $plugin = shift;
891 14         23 my $require_role = shift;
892 14         26 my $coderef = shift;
893 14         27 my $mode = shift;
894              
895 14 100       60 my @role_list =
896             ref $require_role eq 'ARRAY'
897             ? @$require_role
898             : $require_role;
899              
900             return sub {
901 21 100   21   71217 return $plugin->_check_for_login( $coderef )
902             unless $plugin->logged_in_user;
903              
904 15         34 my $role_match;
905              
906             # this is a private method and we should never need 'else'
907             # uncoverable branch false count:3
908 15 100       65 if ( $mode eq 'single' ) {
    100          
    50          
909 9         28 for ( $plugin->user_roles ) {
910 18 100 50     48 $role_match++ and last if _smart_match( $_, $require_role );
911             }
912             }
913             elsif ( $mode eq 'any' ) {
914 2         8 my %role_ok = map { $_ => 1 } @role_list;
  4         16  
915 2         9 for ( $plugin->user_roles ) {
916 4 100 50     23 $role_match++ and last if $role_ok{$_};
917             }
918             }
919             elsif ( $mode eq 'all' ) {
920 4         9 $role_match++;
921 4         13 for my $role (@role_list) {
922 8 100       27 if ( !$plugin->user_has_role($role) ) {
923 2         6 $role_match = 0;
924 2         6 last;
925             }
926             }
927             }
928              
929 15 100       66 if ($role_match) {
930              
931             # We're happy with their roles, so go head and execute the route
932             # handler coderef.
933 10         53 return $coderef->($plugin);
934             }
935              
936 5         111 $plugin->execute_plugin_hook( 'permission_denied', $coderef );
937              
938             # TODO: see if any code executed by that hook set up a response
939              
940 5         1312 $plugin->app->response->status(403);
941 5         441 my $options;
942 5         98 my $view = $plugin->denied_page;
943 5         128 my $template_engine = $plugin->app->template_engine;
944 5         52 my $path = $template_engine->view_pathname($view);
945 5 50       423 if ( !$template_engine->pathname_exists($path) ) {
946 5         162 $plugin->app->log(
947             debug => "app has no denied_page template defined" );
948 5         2564 $options->{content} = $plugin->_render_template('login_denied.tt');
949 5         1742 undef $view;
950             }
951 5         42 return $plugin->app->template( $view, undef, $options );
952 14         214 };
953             }
954              
955             sub _check_for_login {
956 33     33   94 my ( $plugin, $coderef ) = @_;
957 33         626 $plugin->execute_plugin_hook( 'login_required', $coderef );
958              
959             # TODO: see if any code executed by that hook set up a response
960              
961 33         7651 my $request = $plugin->app->request;
962              
963 33 100       567 if ( $plugin->login_without_redirect ) {
964 8         77 my $tokens = {
965             login_failed => $request->var('login_failed'),
966             reset_password_handler => $plugin->reset_password_handler
967             };
968              
969             # The WWW-Authenticate header added varies depending on whether
970             # the client is a robot or not.
971 8         215 my $ua = HTTP::BrowserDetect->new( $request->env->{HTTP_USER_AGENT} );
972 8         1476 my $base = $request->base;
973 8         2154 my $auth_method;
974              
975 8 100 66     28 if ( !$ua->browser_string || $ua->robot ) {
976 7         80 $auth_method = $auth_method = qq{Basic realm="$base"};
977             }
978             else {
979 1         257 $auth_method = qq{FormBasedLogin realm="$base", }
980             . q{comment="use form to log in"};
981             }
982              
983 8         187 $plugin->app->response->status(401);
984 8         682 $plugin->app->response->push_header(
985             'WWW-Authenticate' => $auth_method );
986              
987             # If this is the first attempt to reach a protected page and *not*
988             # a failed passthrough login then we need to stash method and params.
989 8 100       1115 if ( !$request->var('login_failed') ) {
990 5         105 $plugin->app->session->write(
991             '__auth_extensible_method' => lc($request->method) );
992             $plugin->app->session->write(
993 5         415 '__auth_extensible_params' => \%{ $request->params } );
  5         35  
994             }
995              
996 8         313 return $plugin->_render_login_page( 'transparent_login.tt', $tokens );
997             }
998              
999             # old-fashioned redirect to login page with return_url set
1000 25         326 my $forward = $request->path;
1001 25 50       256 $forward .= "?".$request->query_string
1002             if $request->query_string;
1003 25         601 return $plugin->app->redirect(
1004             $request->uri_for(
1005             # Do not use request_uri, as it is the raw string sent by the
1006             # browser, not taking into account the application mount point.
1007             # This means that when it is then concatenated with the base URL,
1008             # the application mount point is specified twice. See GH PR #81
1009             $plugin->login_page, { return_url => $forward }
1010             )
1011             );
1012             }
1013              
1014             sub _render_login_page {
1015 24     24   96 my ( $plugin, $default_template, $tokens ) = @_;
1016              
1017             # If app has its own login page view then use it
1018             # otherwise render our internal one and pass that to 'template'.
1019 24         434 my ( $view, $options ) = ( $plugin->login_template, {} );
1020 24         640 my $template_engine = $plugin->app->template_engine;
1021 24         227 my $path = $template_engine->view_pathname($view);
1022 24 100       1740 if ( !$template_engine->pathname_exists($path) ) {
1023 23         663 $plugin->app->log( debug => "app has no login template defined" );
1024             $options->{content} =
1025 23         12397 $plugin->_render_template( $default_template, $tokens );
1026 23         159508 undef $view;
1027             }
1028 24         210 return $plugin->app->template( $view, $tokens, $options );
1029             }
1030              
1031             sub _default_email_password_reset {
1032 0     0   0 my ( $plugin, %options ) = @_;
1033              
1034 0         0 my %message;
1035 0 0       0 if ( my $password_reset_text = $plugin->password_reset_text ) {
1036 13     13   122 no strict 'refs';
  13         30  
  13         5918  
1037 0         0 %message = &{$password_reset_text}( $plugin, %options );
  0         0  
1038             }
1039             else {
1040 0         0 my $site = $plugin->app->request->uri_base;
1041 0   0     0 my $appname = $plugin->app->config->{appname} || '[unknown]';
1042 0         0 $message{subject} = "Password reset request";
1043 0         0 $message{from} = $plugin->mail_from;
1044 0         0 $message{plain} = <<__EMAIL;
1045             A request has been received to reset your password for $appname. If
1046             you would like to do so, please follow the link below:
1047              
1048             $site/login/$options{code}
1049             __EMAIL
1050             }
1051              
1052 0         0 $plugin->_send_email( to => $options{email}, %message );
1053             }
1054              
1055             sub _render_template {
1056 30     30   96 my ( $plugin, $view, $tokens ) = @_;
1057 30   100     128 $tokens ||= +{};
1058              
1059 30         170 my $template =
1060             path( dist_dir('Dancer2-Plugin-Auth-Extensible'), 'views', $view );
1061              
1062 30         8785 $plugin->_template_tiny->render( $template, $tokens );
1063             }
1064              
1065             sub _default_login_page {
1066 16     16   247 my $plugin = shift;
1067 16         59 my $request = $plugin->app->request;
1068              
1069             # Simple escape of new_password param.
1070             # This only works with the default password generator but since we
1071             # are planning to remove password generation in favour of user-specified
1072             # password on reset then this will do for now.
1073 16         72 my $new_password = $request->parameters->get('new_password');
1074 16 100       1772 if ( defined $new_password ) {
1075 1         6 $new_password =~ s/[^a-zA-Z0-9]//g;
1076             }
1077              
1078             # Make sure all tokens are escaped in some way.
1079 16         345 my $tokens = {
1080             loginpage => uri_escape( $plugin->login_page ),
1081             login_failed => !!$request->var('login_failed'),
1082             new_password => $new_password,
1083             password_code_valid =>
1084             !!$request->parameters->get('password_code_valid'),
1085             reset_sent => !!$request->parameters->get('reset_sent'),
1086             reset_password_handler => !!$plugin->reset_password_handler,
1087             return_url => uri_escape( $request->parameters->get('return_url') ),
1088             };
1089              
1090 16         1787 return $plugin->_render_login_page( 'login.tt', $tokens );
1091             }
1092              
1093             sub _default_permission_denied_page {
1094 2     2   66 shift->_render_template( 'login_denied.tt' );
1095             }
1096              
1097             sub _default_welcome_send {
1098 0     0   0 my ( $plugin, %options ) = @_;
1099              
1100 0         0 my %message;
1101 0 0       0 if ( my $welcome_text = $plugin->welcome_text ) {
1102 13     13   114 no strict 'refs';
  13         37  
  13         9882  
1103 0         0 %message = &{$welcome_text}( $plugin, %options );
  0         0  
1104             }
1105             else {
1106 0         0 my $site = $plugin->app->request->base;
1107 0         0 my $host = $site->host;
1108 0   0     0 my $appname = $plugin->app->config->{appname} || '[unknown]';
1109 0         0 my $reset_link = $site . "login/$options{code}";
1110 0         0 $message{subject} = "Welcome to $host";
1111 0         0 $message{from} = $plugin->mail_from;
1112 0         0 $message{plain} = <<__EMAIL;
1113             An account has been created for you at $host. If you would like
1114             to accept this, please follow the link below to set a password:
1115              
1116             $reset_link
1117             __EMAIL
1118             }
1119              
1120 0         0 $plugin->_send_email( to => $options{email}, %message );
1121             }
1122              
1123             sub _email_mail_message {
1124 0     0   0 my ( $plugin, %params ) = @_;
1125              
1126 0   0     0 my $mailer_options = $plugin->mailer->{options} || {};
1127              
1128 0         0 my @parts;
1129              
1130             push @parts,
1131             Mail::Message::Body::String->new(
1132             mime_type => 'text/plain',
1133             disposition => 'inline',
1134             data => $params{plain},
1135 0 0       0 ) if ( $params{plain} );
1136              
1137             push @parts,
1138             Mail::Message::Body::String->new(
1139             mime_type => 'text/html',
1140             disposition => 'inline',
1141             data => $params{html},
1142 0 0       0 ) if ( $params{html} );
1143              
1144 0 0       0 @parts or croak "No plain or HTML email text supplied";
1145              
1146 0 0       0 my $content_type = @parts > 1 ? 'multipart/alternative' : $parts[0]->type;
1147              
1148             Mail::Message->build(
1149             To => $params{to},
1150             Subject => $params{subject},
1151             From => $params{from},
1152 0         0 'Content-Type' => $content_type,
1153             attach => \@parts,
1154             )->send(%$mailer_options);
1155             }
1156              
1157             sub _send_email {
1158 0     0   0 my $plugin = shift;
1159              
1160 0 0       0 my $mailer = $plugin->mailer or croak "No mailer configured";
1161              
1162             my $module = $mailer->{module}
1163 0 0       0 or croak "No email module specified for mailer";
1164              
1165 0 0       0 if ( $module eq 'Mail::Message' ) {
1166              
1167             # require Mail::Message;
1168 0         0 require Mail::Message::Body::String;
1169 0         0 return $plugin->_email_mail_message(@_);
1170             }
1171             else {
1172 0         0 croak "No support for $module. Please submit a PR!";
1173             }
1174             }
1175              
1176             sub _return_url {
1177 61     61   175 my $app = shift;
1178 61 100 100     350 my $return_url = $app->request->query_parameters->get('return_url')
1179             || $app->request->body_parameters->get('return_url')
1180             or return undef;
1181 6         679 $return_url = uri_unescape($return_url);
1182 6         86 my $uri = URI->new($return_url);
1183             # Construct a URL using uri_for, which ensures that the correct base domain
1184             # is used (preventing open URL redirection attacks). The query needs to be
1185             # parsed and passed as an option, otherwise it is not encoded properly
1186 6         358 return $app->request->uri_for($uri->path, $uri->query_form_hash);
1187             }
1188              
1189             #
1190             # routes
1191             #
1192              
1193             # implementation of logout route
1194             sub _logout_route {
1195 28     28   80482 my $app = shift;
1196 28         104 my $req = $app->request;
1197 28         133 my $plugin = $app->with_plugin('Auth::Extensible');
1198              
1199 28         2209 $plugin->execute_plugin_hook( 'before_logout' );
1200              
1201 28         6820 $app->destroy_session;
1202              
1203 28 100       45934 if ( my $url = _return_url($app) ) {
    50          
1204 2         1136 $app->redirect( $url );
1205             }
1206             elsif ($plugin->exit_page) {
1207 26         3016 $app->redirect($plugin->exit_page);
1208             }
1209             else {
1210             # TODO: perhaps make this more configurable, perhaps by attempting to
1211             # render a template first.
1212 0         0 return "OK, logged out successfully.";
1213             }
1214             }
1215              
1216             # implementation of post login route
1217             sub _post_login_route {
1218 39     39   242553 my $app = shift;
1219 39         188 my $plugin = $app->with_plugin('Auth::Extensible');
1220 39         2485 my $params = $app->request->body_parameters->as_hashref;
1221              
1222             # First check for password reset request, if applicable
1223 39 100 100     1430 if ( $plugin->reset_password_handler && $params->{submit_reset} ) {
1224 2         38 my $username = $params->{username_reset};
1225 2 50       10 croak "Attempt to pass reference to reset blocked" if ref $username;
1226 2         13 $plugin->password_reset_send( username => $username );
1227 2         49 return $app->forward(
1228             $plugin->login_page,
1229             { reset_sent => 1 },
1230             { method => 'GET' }
1231             );
1232             }
1233              
1234             # Then for a password reset itself (confirmed by POST request)
1235             my ($code) =
1236             $plugin->reset_password_handler
1237             && $params->{confirm_reset}
1238 37   66     1194 && $app->request->splat;
1239              
1240 37 100       542 if ($code) {
1241 13     13   107 no strict 'refs';
  13         36  
  13         8415  
1242 2         5 my $randompw = &{ $plugin->password_generator };
  2         42  
1243 2 100       486 if (my $username = $plugin->user_password( code => $code, new_password => $randompw ) ) {
1244             # Support a custom 'Change password' page or other app-based
1245             # intervention after a successful reset code has been applied
1246 1         4 foreach my $realm_check (@{ $plugin->realm_names }) { # $params->{realm} isn't defined at this point...
  1         20  
1247 3         15 my $provider = $plugin->auth_provider($realm_check);
1248 3 100       12 $params->{realm} = $realm_check if $provider->get_user_details($username);
1249             }
1250              
1251             $plugin->execute_plugin_hook( 'after_reset_code_success',
1252 1         28 { username => $username, password => $randompw, realm => $params->{realm} } );
1253              
1254 1         269 return $app->forward(
1255             $plugin->login_page,
1256             { new_password => $randompw },
1257             { method => 'GET' }
1258             );
1259             }
1260             }
1261              
1262             # For security, ensure the username and password are straight scalars; if
1263             # the app is using a serializer and we were sent a blob of JSON, they could
1264             # have come from that JSON, and thus could be hashrefs (JSON SQL injection)
1265             # - for database providers, feeding a carefully crafted hashref to the SQL
1266             # builder could result in different SQL to what we'd expect.
1267             # For instance, if we pass password => params->{password} to an SQL builder,
1268             # we'd expect the query to include e.g. "WHERE password = '...'" (likely
1269             # with paremeterisation) - but if params->{password} was something
1270             # different, e.g. { 'like' => '%' }, we might end up with some SQL like
1271             # WHERE password LIKE '%' instead - which would not be a Good Thing.
1272 36   66     187 my $username = $params->{username} || $params->{__auth_extensible_username};
1273 36   66     155 my $password = $params->{password} || $params->{__auth_extensible_password};
1274              
1275 36         132 for ( $username, $password ) {
1276 72 50       255 if ( ref $_ ) {
1277              
1278             # TODO: handle more cleanly
1279 0         0 croak "Attempt to pass a reference as username/password blocked";
1280             }
1281             }
1282              
1283 36 100       180 if ( $plugin->logged_in_user ) {
1284             # uncoverable condition false
1285 1   33     5 $app->redirect( _return_url($app) || $plugin->user_home_page );
1286             }
1287              
1288 35   66     261 my $auth_realm = $params->{realm} || $params->{__auth_extensible_realm};
1289 35         203 my ( $success, $realm ) =
1290             $plugin->authenticate_user( $username, $password, $auth_realm );
1291              
1292 35 100       153 if ($success) {
1293              
1294             # change session ID if we have a new enough D2 version with support
1295 28 50       365 $plugin->app->change_session_id
1296             if $plugin->app->can('change_session_id');
1297              
1298 28         10169 $app->session->write( logged_in_user => $username );
1299 28         2677 $app->session->write( logged_in_user_realm => $realm );
1300 28         1851 $app->log( core => "Realm is $realm" );
1301 28         1997 $plugin->execute_plugin_hook( 'after_login_success' );
1302             # uncoverable condition false
1303 28   66     7021 $app->redirect( _return_url($app) || $plugin->user_home_page );
1304             }
1305             else {
1306 7         50 $app->request->vars->{login_failed}++;
1307 7         170 $app->forward(
1308             $plugin->login_page,
1309             { login_failed => 1 },
1310             { method => 'GET' }
1311             );
1312             }
1313             }
1314              
1315             #
1316             # private functions
1317             #
1318              
1319             sub _default_password_generator {
1320 2     2   54 Session::Token->new( length => 8 )->get;
1321             }
1322              
1323             sub _reset_code {
1324 7     7   49 Session::Token->new( length => 32 )->get;
1325             }
1326              
1327             # Replacement for much maligned and misunderstood smartmatch operator
1328             sub _smart_match {
1329 18     18   43 my ( $got, $want ) = @_;
1330 18 100       50 if ( !ref $want ) {
    50          
    0          
1331 14         55 return $got eq $want;
1332             }
1333             elsif ( ref $want eq 'Regexp' ) {
1334 4         32 return $got =~ $want;
1335             }
1336             elsif ( ref $want eq 'ARRAY' ) {
1337 0           return grep { $_ eq $got } @$want;
  0            
1338             }
1339             else {
1340 0           carp "Don't know how to match against a " . ref $want;
1341             }
1342             }
1343              
1344             =head1 NAME
1345              
1346             Dancer2::Plugin::Auth::Extensible - extensible authentication framework for Dancer2 apps
1347              
1348             =head1 DESCRIPTION
1349              
1350             A user authentication and authorisation framework plugin for Dancer2 apps.
1351              
1352             Makes it easy to require a user to be logged in to access certain routes,
1353             provides role-based access control, and supports various authentication
1354             methods/sources (config file, database, Unix system users, etc).
1355              
1356             Designed to support multiple authentication realms and to be as extensible as
1357             possible, and to make secure password handling easy. The base class for auth
1358             providers makes handling C<RFC2307>-style hashed passwords really simple, so you
1359             have no excuse for storing plain-text passwords. A simple script called
1360             B<dancer2-generate-crypted-password> to generate
1361             RFC2307-style hashed passwords is included, or you can use L<Crypt::SaltedHash>
1362             yourself to do so, or use the C<slappasswd> utility if you have it installed.
1363              
1364             =head1 SYNOPSIS
1365              
1366             Configure the plugin to use the authentication provider class you wish to use:
1367              
1368             plugins:
1369             Auth::Extensible:
1370             realms:
1371             users:
1372             provider: Config
1373             ....
1374              
1375             The configuration you provide will depend on the authentication provider module
1376             in use. For a simple example, see
1377             L<Dancer2::Plugin::Auth::Extensible::Provider::Config>.
1378              
1379             Define that a user must be logged in and have the proper permissions to
1380             access a route:
1381              
1382             get '/secret' => require_role Confidant => sub { tell_secrets(); };
1383              
1384             Define that a user must be logged in to access a route - and find out who is
1385             logged in with the C<logged_in_user> keyword:
1386              
1387             get '/users' => require_login sub {
1388             my $user = logged_in_user;
1389             return "Hi there, $user->{username}";
1390             };
1391              
1392             =head1 AUTHENTICATION PROVIDERS
1393              
1394             For flexibility, this authentication framework uses simple authentication
1395             provider classes, which implement a simple interface and do whatever is required
1396             to authenticate a user against the chosen source of authentication.
1397              
1398             For an example of how simple provider classes are, so you can build your own if
1399             required or just try out this authentication framework plugin easily,
1400             see L<Dancer2::Plugin::Auth::Extensible::Provider::Config>.
1401              
1402             This framework supplies the following providers out-of-the-box:
1403              
1404             =over 4
1405              
1406             =item L<Dancer2::Plugin::Auth::Extensible::Provider::Unix>
1407              
1408             Authenticates users using system accounts on Linux/Unix type boxes
1409              
1410             =item L<Dancer2::Plugin::Auth::Extensible::Provider::Config>
1411              
1412             Authenticates users stored in the app's config
1413              
1414             =back
1415              
1416             The following external providers are also available on the CPAN:
1417              
1418             =over 4
1419              
1420             =item L<Dancer2::Plugin::Auth::Extensible::Provider::DBIC>
1421              
1422             Authenticates users stored in a database table using L<Dancer2::Plugin::DBIC>
1423              
1424             =item L<Dancer2::Plugin::Auth::Extensible::Provider::Database>
1425              
1426             Authenticates users stored in a database table
1427              
1428             =item L<Dancer2::Plugin::Auth::Extensible::Provider::IMAP>
1429              
1430             Authenticates users via in an IMAP server.
1431              
1432             =item L<Dancer2::Plugin::Auth::Extensible::Provider::LDAP>
1433              
1434             Authenticates users stored in an LDAP directory.
1435              
1436             =item L<Dancer2::Plugin::Auth::Extensible::Provider::Usergroup>
1437              
1438             An alternative L<Dancer2::Plugin::DBIC>-based provider.
1439              
1440             =back
1441              
1442             Need to write your own? Just create a new provider class which consumes
1443             L<Dancer2::Plugin::Auth::Extensible::Role::Provider> and implements the
1444             required methods, and you're good to go!
1445              
1446             =head1 CONTROLLING ACCESS TO ROUTES
1447              
1448             Keywords are provided to check if a user is logged in / has appropriate roles.
1449              
1450             =head2 require_login - require the user to be logged in
1451              
1452             get '/dashboard' => require_login sub { .... };
1453              
1454             If the user is not logged in, they will be redirected to the login page URL to
1455             log in. The default URL is C</login> - this may be changed with the
1456             C<login_page> option.
1457              
1458             =head2 require_role - require the user to have a specified role
1459              
1460             get '/beer' => require_role BeerDrinker => sub { ... };
1461              
1462             Requires that the user be logged in as a user who has the specified role. If
1463             the user is not logged in, they will be redirected to the login page URL. If
1464             they are logged in, but do not have the required role, they will be redirected
1465             to the access denied URL.
1466              
1467             If C<disable_roles> configuration option is set to a true value then using
1468             L</require_role> will cause the application to croak on load.
1469              
1470             =head2 require_any_role - require the user to have one of a list of roles
1471              
1472             get '/drink' => require_any_role [qw(BeerDrinker VodaDrinker)] => sub {
1473             ...
1474             };
1475              
1476             Requires that the user be logged in as a user who has any one (or more) of the
1477             roles listed. If the user is not logged in, they will be redirected to the
1478             login page URL. If they are logged in, but do not have any of the specified
1479             roles, they will be redirected to the access denied URL.
1480              
1481             If C<disable_roles> configuration option is set to a true value then using
1482             L</require_any_role> will cause the application to croak on load.
1483              
1484             =head2 require_all_roles - require the user to have all roles listed
1485              
1486             get '/foo' => require_all_roles [qw(Foo Bar)] => sub { ... };
1487              
1488             Requires that the user be logged in as a user who has all of the roles listed.
1489             If the user is not logged in, they will be redirected to the login page URL. If
1490             they are logged in but do not have all of the specified roles, they will be
1491             redirected to the access denied URL.
1492              
1493             If C<disable_roles> configuration option is set to a true value then using
1494             L</require_all_roles> will cause the application to croak on load.
1495              
1496             =head1 NO-REDIRECT LOGIN
1497              
1498             By default when a page is requested that requires login and the user is not
1499             logged in then the plugin redirects the user to the L</login_page> and sets
1500             C<return_url> to the page originally requested. After successful login the
1501             user is redirected to the originally-requested page.
1502              
1503             As an alternative if L</login_without_redirect> is true then the login
1504             process happens with no redirects. Instead a C<401> C<Unauthorized> code
1505             is returned and a login page is displayed. This login page is posted to the
1506             original URI and on successful login an internal L<Dancer2::Manual/forward>
1507             is performed so that the originally requested page is displayed. Any
1508             L<Dancer2::Manual/params> from the original request are added to the
1509             forward so that they are available to the page's route handler either using
1510             L<Dancer2::Manual/params> or L<Dancer2::Manual/query_parameters>.
1511              
1512             This relies on the login form having no C<action> set and also it must use
1513             C<__auth_extensible_username> and C<__auth_extensible_password> input names.
1514             Optionally C<__auth_extensible_realm> can also be used in a custom login
1515             page.
1516              
1517             See L<http://shadow.cat/blog/matt-s-trout/humane-login-screens/> for the
1518             original idea for this functionality.
1519              
1520             =head1 CUSTOMISING C</login> AND C</login/denied>
1521              
1522             =head2 login_template
1523              
1524             The L</login_template> setting determines the name of the view you use
1525             for your custom login page. If this view exists in your application then it
1526             will be used instead of the default login template.
1527              
1528             If you are using L</login_without_redirect> and assuming you are using
1529             L<Template::Toolkit> then your custom login page should be something like this:
1530              
1531             <h1>Login Required</h1>
1532              
1533             <p>You need to log in to continue.</p>
1534              
1535             [%- IF login_failed -%]
1536             <p>LOGIN FAILED</p>
1537             [%- END -%]
1538              
1539             <form method="post">
1540             <label for="username">Username:</label>
1541             <input type="text" name="__auth_extensible_username" id="username">
1542             <br />
1543             <label for="password">Password:</label>
1544             <input type="password" name="__auth_extensible_password" id="password">
1545             <br />
1546             <input type="submit" value="Login">
1547             </form>
1548              
1549             [%- IF reset_password_handler -%]
1550             <form method="post" action="[% login_page %]">
1551             <h2>Password reset</h2>
1552             <p>Enter your username to obtain an email to reset your password</p>
1553             <label for="username_reset">Username:</label>
1554             <input type="text" name="username_reset" id="username_reset">
1555             <input type="submit" name="submit_reset" value="Submit">
1556             </form>
1557             [%- END -%]
1558              
1559             If you are B<not> using L</login_without_redirect> and assuming you are using
1560             L<Template::Toolkit> then your custom login page should be something like this:
1561              
1562             <h1>Login Required</h1>
1563              
1564             <p>You need to log in to continue.</p>
1565              
1566             [%- IF login_failed -%]
1567             <p>LOGIN FAILED</p>
1568             [%- END -%]
1569              
1570             <form method="post">
1571             <label for="username">Username:</label>
1572             <input type="text" name="username" id="username">
1573             <br />
1574             <label for="password">Password:</label>
1575             <input type="password" name="password" id="password">
1576             <br />
1577             <input type="submit" value="Login">
1578              
1579             [%- IF return_url -%]
1580             <input type="hidden" name="return_url" value="[% return_url %]">
1581             [%- END -%]
1582              
1583             [%- IF reset_password_handler -%]
1584             <h2>Password reset</h2>
1585             <p>Enter your username to obtain an email to reset your password</p>
1586             <label for="username_reset">Username:</label>
1587             <input type="text" name="username_reset" id="username_reset">
1588             <input type="submit" name="submit_reset" value="Submit">
1589             [%- END -%]
1590              
1591             </form>
1592              
1593             =head2 Replacing the default C< /login > and C< /login/denied > routes
1594              
1595             By default, the plugin adds a route to present a simple login form at that URL.
1596             If you would rather add your own, set the C<no_default_pages> setting to a true
1597             value, and define your own route which responds to C</login> with a login page.
1598             Alternatively you can let DPAE add the routes and handle the status codes, etc.
1599             and simply define the setting C<login_page_handler> and/or
1600             C<permission_denied_page_handler> with the name of a subroutine to be called to
1601             handle the route. Note that it must be a fully qualified sub. E.g.
1602              
1603             plugins:
1604             Auth::Extensible:
1605             login_page_handler: 'My::App::login_page_handler'
1606             permission_denied_page_handler: 'My::App::permission_denied_page_handler'
1607              
1608             Then in your code you might simply use a template:
1609              
1610             sub login_page_handler {
1611             my $return_url = query_parameters->get('return_url');
1612             template
1613             'account/login',
1614             { title => 'Sign in',
1615             return_url => $return_url,
1616             },
1617             { layout => 'login.tt',
1618             };
1619             }
1620              
1621             sub permission_denied_page_handler {
1622             template 'account/login';
1623             }
1624              
1625             and your account/login.tt template might look like:
1626              
1627             [% IF vars.login_failed %]
1628             <div class="alert alert-danger">
1629             <strong>Login Failed</strong> Try again
1630             <button type="button" class="close" data-dismiss="alert" aria-label="Close">
1631             <span aria-hidden="true">&times;</span>
1632             </button>
1633             </div>
1634             [% END %]
1635              
1636             <form method = "post" lpformnum="1" class="form-signin">
1637             <h2 class="form-signin-heading">Please sign in</h2>
1638             <label for="username" class="sr-only">Username</label>
1639             <input type="text" name="username" id="username" class="form-control" placeholder="User name" required autofocus>
1640             <label for="password" class="sr-only">Password</label>
1641             <input type="password" name="password" id="password" class="form-control" placeholder="Password" required>
1642             <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
1643             <br>
1644             <input type="hidden" name="return_url" value="[% return_url %]">
1645             </form>
1646              
1647              
1648             If the user is logged in, but tries to access a route which requires a specific
1649             role they don't have, they will be redirected to the "permission denied" page
1650             URL, which defaults to C</login/denied> but may be changed using the
1651             C<denied_page> option.
1652              
1653             Again, by default a route is added to respond to that URL with a default page;
1654             again, you can disable this by setting C<no_default_pages> and creating your
1655             own.
1656              
1657             This would still leave the routes C<post '/login'> and C<any '/logout'>
1658             routes in place. To disable them too, set the option C<no_login_handler>
1659             to a true value. In this case, these routes should be defined by the user,
1660             and should do at least the following:
1661              
1662             post '/login' => sub {
1663             my ($success, $realm) = authenticate_user(
1664             params->{username}, params->{password}
1665             );
1666             if ($success) {
1667             # change session ID if we have a new enough D2 version with support
1668             # (security best practice on privilege level change)
1669             app->change_session_id
1670             if app->can('change_session_id');
1671             session logged_in_user => params->{username};
1672             session logged_in_user_realm => $realm;
1673             # other code here
1674             } else {
1675             # authentication failed
1676             }
1677             };
1678            
1679             any '/logout' => sub {
1680             app->destroy_session;
1681             };
1682              
1683             If you want to use the default C<post '/login'> and C<any '/logout'> routes
1684             you can configure them. See below.
1685              
1686             The default routes also contain functionality for a user to perform password
1687             resets. See the L<PASSWORD RESETS> documentation for more details.
1688              
1689             =head1 KEYWORDS
1690              
1691             The following keywords are provided in additional to the route decorators
1692             specified in L</CONTROLLING ACCESS TO ROUTES>:
1693              
1694             =head2 logged_in_user
1695              
1696             Returns a hashref of details of the currently logged-in user or some kind of
1697             user object, if there is one.
1698              
1699             The details you get back will depend upon the authentication provider in use.
1700              
1701             =head2 get_user_details
1702              
1703             Returns a hashref of details of the specified user. The realm can optionally
1704             be specified as the second parameter. If the realm is not specified, each
1705             realm will be checked, and the first matching user will be returned.
1706              
1707             The details you get back will depend upon the authentication provider in use.
1708              
1709             =head2 user_has_role
1710              
1711             Check if a user has the role named.
1712              
1713             By default, the currently-logged-in user will be checked, so you need only name
1714             the role you're looking for:
1715              
1716             if (user_has_role('BeerDrinker')) { pour_beer(); }
1717              
1718             You can also provide the username to check;
1719              
1720             if (user_has_role($user, $role)) { .... }
1721              
1722             If C<disable_roles> configuration option is set to a true value then using
1723             L</user_has_role> will cause the application to croak at runtime.
1724              
1725             =head2 user_roles
1726              
1727             Returns a list of the roles of a user.
1728              
1729             By default, roles for the currently-logged-in user will be checked;
1730             alternatively, you may supply a username to check.
1731              
1732             Returns a list or arrayref depending on context.
1733              
1734             If C<disable_roles> configuration option is set to a true value then using
1735             L</user_roles> will cause the application to croak at runtime.
1736              
1737             =head2 authenticate_user
1738              
1739             Usually you'll want to let the built-in login handling code deal with
1740             authenticating users, but in case you need to do it yourself, this keyword
1741             accepts a username and password, and optionally a specific realm, and checks
1742             whether the username and password are valid.
1743              
1744             For example:
1745              
1746             if (authenticate_user($username, $password)) {
1747             ...
1748             }
1749              
1750             If you are using multiple authentication realms, by default each realm will be
1751             consulted in turn. If you only wish to check one of them (for instance, you're
1752             authenticating an admin user, and there's only one realm which applies to them),
1753             you can supply the realm as an optional third parameter.
1754              
1755             In boolean context, returns simply true or false; in list context, returns
1756             C<($success, $realm)>.
1757              
1758             =head2 logged_in_user_lastlogin
1759              
1760             Returns (as a DateTime object) the time of the last successful login of the
1761             current logged in user.
1762              
1763             To enable this functionality, set the configuration key C<record_lastlogin> to
1764             a true value. The backend provider must support write access for a user and
1765             have lastlogin functionality implemented.
1766              
1767             =head2 update_user
1768              
1769             Updates a user's details. If the authentication provider supports it, this
1770             keyword allows a user's details to be updated within the backend data store.
1771              
1772             In order to update the user's details, the keyword should be called with the
1773             username to be updated, followed by a hash of the values to be updated. Note
1774             that whilst the password can be updated using this method, any new value will
1775             be stored directly into the provider as-is, not encrypted. It is recommended to
1776             use L</user_password> instead.
1777              
1778             If only one realm is configured then this will be used to search for the user.
1779             Otherwise, the realm must be specified with the realm key.
1780              
1781             # Update user, only one realm configured
1782             update_user "jsmith", surname => "Smith"
1783              
1784             # Update a user's username, more than one realm
1785             update_user "jsmith", realm => "dbic", username => "jjones"
1786              
1787             The updated user's details are returned, as per L<logged_in_user>.
1788              
1789             =head2 update_current_user
1790              
1791             The same as L<update_user>, but does not take a username as the first parameter,
1792             instead updating the currently logged-in user.
1793              
1794             # Update user, only one realm configured
1795             update_current_user surname => "Smith"
1796              
1797             The updated user's details are returned, as per L<logged_in_user>.
1798              
1799             =head2 create_user
1800              
1801             Creates a new user, if the authentication provider supports it. Optionally
1802             sends a welcome message with a password reset request, in which case an
1803             email key must be provided.
1804              
1805             This function works in the same manner as L<update_user>, except that
1806             the username key is mandatory. As with L<update_user>, it is recommended
1807             not to set a password directly using this method, otherwise it will be
1808             stored in plain text.
1809              
1810             The realm to use must be specified with the key C<realm> if there is more
1811             than one realm configured.
1812              
1813             # Create new user
1814             create_user username => "jsmith", realm => "dbic", surname => "Smith"
1815              
1816             # Create new user and send welcome email
1817             create_user username => "jsmith", email => "john@you.com", email_welcome => 1
1818              
1819             On success, the created user's details are returned, as per L<logged_in_user>.
1820              
1821             The text sent in the welcome email can be customised in 2 ways, in the same way
1822             as L<password_reset_send>:
1823              
1824             =over
1825              
1826             =item welcome_send
1827              
1828             This can be used to specify a subroutine that will be called to perform the
1829             entire message construction and email sending. Note that it must be a
1830             fully-qualified sub such as C<My::App:email_welcome_send>. The subroutine will
1831             be passed the dsl as the first parameter, followed by a hash with the keys
1832             C<code>, C<email> and C<user>, which contain the generated reset code, user
1833             email address, and user hashref respectively. For example:
1834              
1835             sub reset_send_handler {
1836             my ($dsl, %params) = @_;
1837             my $user_email = $params{email};
1838             my $reset_code = $params{code};
1839             # Send email
1840             return $result;
1841             }
1842              
1843             =item welcome_text
1844              
1845             This can be used to generate the text for the welcome email, with this module
1846             sending the actual email itself. It must be a fully-qualified sub, as per the
1847             previous option. It will be passed the same parameters as
1848             L<welcome_send>, and should return a hash with the same keys as
1849             L<password_reset_send_email>.
1850              
1851             =back
1852              
1853             =head2 password_reset_send
1854              
1855             L</password_reset_send> sends a user an email with a password reset link. Along
1856             with L</user_password>, it allows a user to reset their password.
1857              
1858             The function must be called with the key C<username> and a value that is the
1859             username. The username specified will be sent an email with a link to reset
1860             their password. Note that the provider being used must return the email address
1861             in the key C<email>, which in the case of a database will normally require that
1862             column to exist in the user's table. The provider must be able to write values
1863             to the user in order for this function to store the generated code.
1864              
1865             If the username is not found, a value of 0 is returned. If the username is
1866             found and the email is sent successfully, 1 is returned. Otherwise undef is
1867             returned. Note: if you are displaying a success message, and you do not want
1868             people to be able to check the existance of a user on your system, then you
1869             should check for the return value being defined, not true. For example:
1870              
1871             say "Success" if defined password_reset_send username => username;
1872              
1873             Note that this still leaves the possibility of checking the existance of a user
1874             if the email send mechanism is failing.
1875              
1876             The realm can also be specified using the key realm:
1877              
1878             password_reset_send username => 'jsmith', realm => 'dbic'
1879              
1880             Default text for the email is automatically produced and emailed. This can be
1881             customized with one of 2 config parameters:
1882              
1883             =over
1884              
1885             =item password_reset_send_email
1886              
1887             This can be used to specify a subroutine that will be called to perform the
1888             entire message construction and email sending. Note that it must be a
1889             fully-qualified sub such as C<My::App:reset_send_handler>. The subroutine will
1890             be passed the dsl as the first parameter, followed by a hash with the keys
1891             C<code> and C<email>, which contain the generated reset code and user email
1892             address respectively. For example:
1893              
1894             sub reset_send_handler {
1895             my ($dsl, %params) = @_;
1896             my $user_email = $params{email};
1897             my $reset_code = $params{code};
1898             # Send email
1899             return $result;
1900             }
1901              
1902             =item password_reset_text
1903              
1904             This can be used to generate the text for the email, with this module sending
1905             the actual email itself. It must be a fully-qualified sub, as per the previous
1906             option. It will be passed the same parameters as L<password_reset_send_email>,
1907             and should return a hash with the following keys:
1908              
1909             =over
1910              
1911             =item subject
1912              
1913             The subject of the email message.
1914              
1915             =item from
1916              
1917             The sender of the email message (optional, can also be specified using
1918             C<mail_from>.
1919              
1920             =item plain
1921              
1922             Plain text for the email. Either this, or html, or both should be returned.
1923              
1924             =item html
1925              
1926             HTML text for the email (optional, as per plain).
1927              
1928             =back
1929              
1930             Here is an example subroutine:
1931              
1932             sub reset_text_handler {
1933             my ($dsl, %params) = @_;
1934             return (
1935             from => '"My name" <myapp@example.com',
1936             subject => 'the subject',
1937             plain => "reset here: $params{code}",
1938             );
1939             }
1940              
1941             # Example configuration
1942              
1943             Auth::Extensible:
1944             mailer:
1945             module: Mail::Message # Module to send email with
1946             options: # Module options
1947             via: sendmail
1948             mail_from: '"My app" <myapp@example.com>'
1949             password_reset_text: MyApp::reset_send
1950              
1951             =back
1952              
1953             =head2 user_password
1954              
1955             This provides various functions to check or reset a user's password, either
1956             from a reset code that was previously send by L<password_reset_send> or
1957             directly by specifying a username and password. Functions that update a
1958             password rely on a provider that has write access to a user's details.
1959              
1960             By default, the user to update is the currently logged-in user. A specific user
1961             can be specified with the key C<username> for a certain username, or C<code>
1962             for a previously sent reset code. Using these parameters on their own will
1963             return the username if it is a valid request.
1964              
1965             If the above parameters are specified with the additional parameter
1966             C<new_password>, then the password will be set to that value, assuming that it
1967             is a valid request.
1968              
1969             The realm can be optionally specified with the keyword C<realm>.
1970              
1971             Examples:
1972              
1973             Check the logged-in user's password:
1974              
1975             user_password password => 'mysecret'
1976              
1977             Check a specific user's password:
1978              
1979             user_password username => 'jsmith', password => 'bigsecret'
1980              
1981             Check a previously sent reset code:
1982              
1983             user_password code => 'XXXX'
1984              
1985             Reset a password with a previously sent code:
1986              
1987             user_password code => 'XXXX', new_password => 'newsecret'
1988              
1989             Change a user's password (username optional)
1990              
1991             user_password username => 'jbloggs', password => 'old', new_password => 'secret'
1992              
1993             Force set a specific user's password, without checking existing password:
1994              
1995             user_password username => 'jbloggs', new_password => 'secret'
1996              
1997             =head2 logged_in_user_password_expired
1998              
1999             Returns true if the password of the currently logged in user has expired. To
2000             use this functionality, the provider must support the C<password_expired>
2001             function, and must be configured accordingly. See the relevant provider for
2002             full configuration details.
2003              
2004             Note that this functionality does B<not> prevent the user accessing any
2005             protected pages, even if the password has expired. This is so that the
2006             developer can still leave some protected routes available, such as a page to
2007             change the password. Therefore, if using this functionality, it is suggested
2008             that a check is done in the C<before> hook:
2009              
2010             hook before => sub {
2011             if (logged_in_user_password_expired)
2012             {
2013             # Redirect to user details page if password expired, but only if that
2014             # is not the currently request page to prevent redirect loops
2015             redirect '/password_update' unless request->uri eq '/password_update';
2016             }
2017             }
2018              
2019             =head2 PASSWORD RESETS
2020              
2021             A variety of functionality is provided to make it easier to manage requests
2022             from users to reset their passwords. The keywords L<password_reset_send> and
2023             L<user_password> form the core of this functionality - see the documentation of
2024             these keywords for full details. This functionality can only be used with a
2025             provider that supports write access.
2026              
2027             When utilising this functionality, it is wise to only allow passwords to be
2028             reset with a POST request. This is because some email scanners "open" links
2029             before delivering the email to the end user. With only a single-use GET
2030             request, this will result in the link being "used" by the time it reaches the
2031             end user, thus rendering it invalid.
2032              
2033             Password reset functionality is also built-in to the default route handlers.
2034             To enable this, set the configuration value C<reset_password_handler> to a true
2035             value (having already configured the mail handler, as per the keyword
2036             documentation above). Once this is done, the default login page will contain
2037             additional form controls to allow the user to enter their username and request
2038             a reset password link.
2039              
2040             By default, the default handlers will generate a random 8 character password using
2041             L<Session::Token>. To use your own function, set C<password_generator> in your
2042             configuration. See the L<SAMPLE CONFIGURATION> for an example.
2043              
2044             If using C<login_page_handler> to replace the default login page, you can still
2045             use the default password reset handlers. Add 2 controls to your form for
2046             submitting a password reset request: a text input called username_reset for the
2047             username, and submit_reset to submit the request. Your login_page_handler is
2048             then passed the following additional params:
2049              
2050             =over
2051              
2052             =item new_password
2053              
2054             Contains the new automatically-generated password, once the password reset has
2055             been performed successfully.
2056              
2057             =item reset_sent
2058              
2059             Is true when a password reset has been emailed to the user.
2060              
2061             =item password_code_valid
2062              
2063             Is true when a valid password reset code has been submitted with a GET request.
2064             In this case, the user should be given the chance to confirm with a POST
2065             request, with a form control called C<confirm_reset>.
2066              
2067             For a full example, see the default handler in this module's code.
2068              
2069             =back
2070              
2071             =head2 SAMPLE CONFIGURATION
2072              
2073             In your application's configuation file:
2074              
2075             session: simple
2076             plugins:
2077             Auth::Extensible:
2078             # Set to 1 if you want to disable the use of roles (0 is default)
2079             # If roles are disabled then any use of role-based route decorators
2080             # will cause app to croak on load. Use of 'user_roles' and
2081             # 'user_has_role' will croak at runtime.
2082             disable_roles: 0
2083             # Set to 1 to use the no-redirect login functionality
2084             login_without_redirect: 0
2085             # Set the view name for a custom login page, defaults to 'login'
2086             login_template: login
2087             # After /login: If no return_url is given: land here ('/' is default)
2088             user_home_page: '/user'
2089             # After /logout: If no return_url is given: land here (no default)
2090             exit_page: '/'
2091              
2092             # Mailer options for reset password and welcome emails
2093             mailer:
2094             module: Mail::Message # Email module to use
2095             options: # Options for module
2096             via: sendmail # Options passed to $msg->send
2097             mail_from: '"App name" <myapp@example.com>' # From email address
2098              
2099             # Set to true to enable password reset code in the default handlers
2100             reset_password_handler: 1
2101             password_generator: My::App::random_pw # Optional random password generator
2102              
2103             # Set to a true value to enable recording of successful last login times
2104             record_lastlogin: 1
2105              
2106             # Password reset functionality
2107             password_reset_send_email: My::App::reset_send # Customise sending sub
2108             password_reset_text: My::App::reset_text # Customise reset text
2109              
2110             # create_user options
2111             welcome_send: My::App::welcome_send # Customise welcome email sub
2112             welcome_text: My::App::welcome_text # Customise welcome email text
2113              
2114             # List each authentication realm, with the provider to use and the
2115             # provider-specific settings (see the documentation for the provider
2116             # you wish to use)
2117             realms:
2118             realm_one:
2119             priority: 3 # Defaults to 0. Realms are checked in descending order
2120             provider: Database
2121             db_connection_name: 'foo'
2122             realm_two:
2123             priority: 0 # Will be checked after realm_one
2124             provider: Config
2125              
2126             B<Please note> that you B<must> have a session provider configured. The
2127             authentication framework requires sessions in order to track information about
2128             the currently logged in user.
2129             Please see L<Dancer2::Core::Session> for information on how to configure session
2130             management within your application.
2131              
2132             =head1 METHODS
2133              
2134             =head2 auth_provider($dsl, $realm)
2135              
2136             Given a realm, returns a configured and ready to use instance of the provider
2137             specified by that realm's config.
2138              
2139             =head1 HOOKS
2140              
2141             This plugin provides the following hooks:
2142              
2143             =head2 before_authenticate_user
2144              
2145             Called at the start of L</authenticate_user>.
2146              
2147             Receives a hash reference of C<username>, C<password> and C<realm>.
2148              
2149             =head2 after_authenticate_user
2150              
2151             Called at the end of L</authenticate_user>.
2152              
2153             Receives a hash reference of C<username>, C<password>, C<realm>, C<errors>
2154             and C<success>.
2155              
2156             C<realm> is the realm that the user authenticated against of undef if auth
2157             failed.
2158              
2159             The value of C<errors> is an array reference of any errors thrown by
2160             authentication providers (if any).
2161              
2162             The value of C<success> is either C<1> or C<0> to show whether or not
2163             authentication was successful.
2164              
2165             =head2 before_create_user
2166              
2167             Called at the start of L</create_user>.
2168              
2169             Receives a hash reference of the arguments passed to L</create_user>.
2170              
2171             =head2 after_create_user
2172              
2173             Called at the end of L</create_user>.
2174              
2175             Receives the requested username, the created user (or undef) and an array
2176             reference of any errors from the main method or from the provider.
2177              
2178             =head2 login_required
2179              
2180             =head2 permission_denied
2181              
2182             =head2 after_reset_code_success
2183              
2184             Called after successful reset code has been provided. Supports a custom 'Change
2185             password' page or other app-based intervention after a successful reset code
2186             has been applied.
2187              
2188             =head2 after_login_success
2189              
2190             Called after successful login just before redirect is called.
2191              
2192             =head2 before_logout
2193              
2194             Called just before the session gets destroyed on logout.
2195              
2196             =head1 AUTHOR
2197              
2198             David Precious, C<< <davidp at preshweb.co.uk> >>
2199              
2200             Dancer2 port of Dancer::Plugin::Auth::Extensible by:
2201              
2202             Stefan Hornburg (Racke), C<< <racke at linuxia.de> >>
2203              
2204             Conversion to Dancer2's new plugin system plus much cleanup & reorg:
2205              
2206             Peter Mottram (SysPete), C<< <peter at sysnix.com> >>
2207              
2208             =head1 BUGS / FEATURE REQUESTS
2209              
2210             This is an early version; there may still be bugs present or features missing.
2211              
2212             This is developed on GitHub - please feel free to raise issues or pull requests
2213             against the repo at:
2214             L<https://github.com/PerlDancer/Dancer2-Plugin-Auth-Extensible>
2215              
2216             =head1 ACKNOWLEDGEMENTS
2217              
2218             Valuable feedback on the early design of this module came from many people,
2219             including Matt S Trout (mst), David Golden (xdg), Damien Krotkine (dams),
2220             Daniel Perrett, and others.
2221              
2222             Configurable login/logout URLs added by Rene (hertell)
2223              
2224             Regex support for require_role by chenryn
2225              
2226             Support for user_roles looking in other realms by Colin Ewen (casao)
2227              
2228             LDAP provider added by Mark Meyer (ofosos)
2229              
2230             Documentation fix by Vince Willems.
2231              
2232             Henk van Oers (GH #8, #13, #55).
2233              
2234             Andrew Beverly (GH #6, #7, #10, #17, #22, #24, #25, #26, #54).
2235             This includes support for creating and editing users and manage user passwords.
2236              
2237             Gabor Szabo (GH #11, #16, #18).
2238              
2239             Evan Brown (GH #20, #32).
2240              
2241             Jason Lewis (Unix provider problem, GH#62).
2242              
2243             Matt S. Trout (mst) for L<Zero redirect login the easy and friendly way|http://shadow.cat/blog/matt-s-trout/humane-login-screens/>.
2244              
2245             Ben Kaufman "whosgonna" (GH#79)
2246              
2247             Dominic Sonntag (GH#70)
2248              
2249             =head1 LICENSE AND COPYRIGHT
2250              
2251             Copyright 2012-16 David Precious.
2252              
2253             This program is free software; you can redistribute it and/or modify it
2254             under the terms of either: the GNU General Public License as published
2255             by the Free Software Foundation; or the Artistic License.
2256              
2257             See http://dev.perl.org/licenses/ for more information.
2258              
2259             =cut
2260              
2261             1; # End of Dancer2::Plugin::Auth::Extensible