File Coverage

blib/lib/Yancy/Plugin/Auth/Basic.pm
Criterion Covered Total %
statement 104 104 100.0
branch 29 30 96.6
condition 10 13 76.9
subroutine 16 16 100.0
pod 1 2 50.0
total 160 165 96.9


line stmt bran cond sub pod time code
1             package Yancy::Plugin::Auth::Basic;
2             our $VERSION = '1.087';
3             # ABSTRACT: (DEPRECATED) A simple auth module for a site
4              
5             #pod =encoding utf8
6             #pod
7             #pod =head1 SYNOPSIS
8             #pod
9             #pod use Mojolicious::Lite;
10             #pod plugin Yancy => {
11             #pod backend => 'pg://localhost/mysite',
12             #pod schema => {
13             #pod users => {
14             #pod required => [ 'username', 'password' ],
15             #pod properties => {
16             #pod id => { type => 'integer', readOnly => 1 },
17             #pod username => { type => 'string' },
18             #pod password => { type => 'string', format => 'password' },
19             #pod },
20             #pod },
21             #pod },
22             #pod };
23             #pod app->yancy->plugin( 'Auth::Basic' => {
24             #pod schema => 'users',
25             #pod username_field => 'username',
26             #pod password_field => 'password',
27             #pod password_digest => {
28             #pod type => 'SHA-1',
29             #pod },
30             #pod } );
31             #pod
32             #pod =head1 DESCRIPTION
33             #pod
34             #pod B: This plugin is deprecated and will be removed in Yancy v2.000. Please
35             #pod switch to the new pluggable auth L or the new password
36             #pod auth L.
37             #pod
38             #pod This plugin provides a basic authentication and authorization scheme for
39             #pod a L site using L. If a user is authenticated, they are
40             #pod then authorized to use the administration application and API.
41             #pod
42             #pod =head1 CONFIGURATION
43             #pod
44             #pod This plugin has the following configuration options.
45             #pod
46             #pod =over
47             #pod
48             #pod =item schema
49             #pod
50             #pod The name of the Yancy schema that holds users. Required.
51             #pod
52             #pod =item username_field
53             #pod
54             #pod The name of the field in the schema which is the user's identifier.
55             #pod This can be a user name, ID, or e-mail address, and is provided by the
56             #pod user during login.
57             #pod
58             #pod This field is optional. If not specified, the schema's ID field will
59             #pod be used. For example, if the schema uses the C field as
60             #pod a unique identifier, we don't need to provide a C.
61             #pod
62             #pod plugin Yancy => {
63             #pod schema => {
64             #pod users => {
65             #pod 'x-id-field' => 'username',
66             #pod properties => {
67             #pod username => { type => 'string' },
68             #pod password => { type => 'string' },
69             #pod },
70             #pod },
71             #pod },
72             #pod };
73             #pod app->yancy->plugin( 'Auth::Basic' => {
74             #pod schema => 'users',
75             #pod password_digest => { type => 'SHA-1' },
76             #pod } );
77             #pod
78             #pod =item password_field
79             #pod
80             #pod The name of the field to use for the user's password. Defaults to C.
81             #pod
82             #pod This field will automatically be set up to use the L filter to
83             #pod properly hash the password when updating it.
84             #pod
85             #pod =item password_digest
86             #pod
87             #pod This is the hashing mechanism that should be used for passwords. There is no
88             #pod default, so you must configure one.
89             #pod
90             #pod This value should be a hash of digest configuration. The one required
91             #pod field is C, and should be a type supported by the L module:
92             #pod
93             #pod =over
94             #pod
95             #pod =item * MD5 (part of core Perl)
96             #pod
97             #pod =item * SHA-1 (part of core Perl)
98             #pod
99             #pod =item * SHA-256 (part of core Perl)
100             #pod
101             #pod =item * SHA-512 (part of core Perl)
102             #pod
103             #pod =item * Bcrypt (recommended)
104             #pod
105             #pod =back
106             #pod
107             #pod Additional fields are given as configuration to the L module.
108             #pod Not all Digest types require additional configuration.
109             #pod
110             #pod # Use Bcrypt for passwords
111             #pod # Install the Digest::Bcrypt module first!
112             #pod app->yancy->plugin( 'Auth::Basic' => {
113             #pod password_digest => {
114             #pod type => 'Bcrypt',
115             #pod cost => 12,
116             #pod salt => 'abcdefgh♥stuff',
117             #pod },
118             #pod } );
119             #pod
120             #pod =item route
121             #pod
122             #pod The root route that this auth module should protect. Defaults to
123             #pod protecting only the Yancy editor application.
124             #pod
125             #pod =back
126             #pod
127             #pod =head2 Sessions
128             #pod
129             #pod This module uses L
130             #pod sessions|https://mojolicious.org/perldoc/Mojolicious/Controller#session>
131             #pod to store the login information in a secure, signed cookie.
132             #pod
133             #pod To configure the default expiration of a session, use
134             #pod L
135             #pod default_expiration|https://mojolicious.org/perldoc/Mojolicious/Sessions#default_expiration>.
136             #pod
137             #pod use Mojolicious::Lite;
138             #pod # Expire a session after 1 day of inactivity
139             #pod app->sessions->default_expiration( 24 * 60 * 60 );
140             #pod
141             #pod =head1 TEMPLATES
142             #pod
143             #pod To override these templates in your application, provide your own
144             #pod template with the same name.
145             #pod
146             #pod =over
147             #pod
148             #pod =item yancy/auth/login.html.ep
149             #pod
150             #pod This template displays the login form. The form should have two fields,
151             #pod C and C, and perform a C request to C<<
152             #pod url_for 'yancy.check_login' >>
153             #pod
154             #pod =item yancy/auth/unauthorized.html.ep
155             #pod
156             #pod This template displays an error message that the user is not authorized
157             #pod to view this page. This most-often appears when the user is not logged in.
158             #pod
159             #pod =item layouts/yancy/auth.html.ep
160             #pod
161             #pod The layout that Yancy uses when displaying the login form, the
162             #pod unauthorized error message, and other auth-related pages.
163             #pod
164             #pod =back
165             #pod
166             #pod =head1 FILTERS
167             #pod
168             #pod This module provides the following filters. See L
169             #pod Configuration> for how to use filters.
170             #pod
171             #pod =head2 auth.digest
172             #pod
173             #pod Run the field value through the configured password L object and
174             #pod store the Base64-encoded result instead.
175             #pod
176             #pod =head1 HELPERS
177             #pod
178             #pod This plugin adds the following Mojolicious helpers:
179             #pod
180             #pod =head2 yancy.auth.route
181             #pod
182             #pod The L that requires
183             #pod authentication. Add your own routes as children of this route to
184             #pod require authentication for your own routes.
185             #pod
186             #pod my $auth_route = $app->yancy->auth->route;
187             #pod $auth_route->get( '/', sub {
188             #pod my ( $c ) = @_;
189             #pod return $c->render(
190             #pod data => 'You are authorized to view this page',
191             #pod );
192             #pod } );
193             #pod
194             #pod =head2 yancy.auth.current_user
195             #pod
196             #pod Get/set the currently logged-in user. Returns C if no user is
197             #pod logged-in.
198             #pod
199             #pod my $user = $c->yancy->auth->current_user
200             #pod || return $c->render( status => 401, text => 'Unauthorized' );
201             #pod
202             #pod To set the current user, pass in the username.
203             #pod
204             #pod $c->yancy->auth->current_user( $username );
205             #pod
206             #pod =head2 yancy.auth.get_user
207             #pod
208             #pod my $user = $c->yancy->auth->get_user( $username );
209             #pod
210             #pod Get a user item by its C.
211             #pod
212             #pod =head2 yancy.auth.check
213             #pod
214             #pod Check a username and password to authenticate a user. Returns true
215             #pod if the user is authenticated, or returns false.
216             #pod
217             #pod B: Does not change the currently logged-in user.
218             #pod
219             #pod if ( $c->yancy->auth->check( $username, $password ) ) {
220             #pod # Authentication succeeded
221             #pod $c->yancy->auth->current_user( $username );
222             #pod }
223             #pod
224             #pod =head2 yancy.auth.clear
225             #pod
226             #pod Clear the currently logged-in user (logout).
227             #pod
228             #pod $c->yancy->auth->clear;
229             #pod
230             #pod =head1 SEE ALSO
231             #pod
232             #pod L, L
233             #pod
234             #pod =cut
235              
236 1     1   975 use Yancy::Util qw( derp );
  1         2  
  1         75  
237 1     1   7 use Mojo::Base 'Mojolicious::Plugin';
  1         4  
  1         9  
238 1     1   218 use Digest;
  1         2  
  1         24  
239 1     1   6 use Yancy::Util qw( currym );
  1         2  
  1         2068  
240              
241             has logout_route =>;
242              
243             sub register {
244 7     7 1 223 my ( $self, $app, $config ) = @_;
245             # Prepare and validate backend data configuration
246             die "Error configuring Auth::Basic plugin: No password digest type defined\n"
247 7 100 66     48 unless $config->{password_digest} && $config->{password_digest}{type};
248              
249             my $coll = $config->{schema} || $config->{collection}
250 6   100     43 || die "Error configuring Auth::Basic plugin: No collection defined\n";
251 5 100       21 die sprintf(
252             q{Error configuring Auth::Basic plugin: Collection "%s" not found}."\n",
253             $coll,
254             ) unless $app->yancy->schema( $coll );
255              
256 4         59 derp "The Auth::Basic plugin is deprecated and will be removed in Yancy v2.000. Please migrate to the Auth::Password module.\n";
257              
258 4         13 my $username_field = $config->{username_field};
259 4   100     18 my $password_field = $config->{password_field} || 'password';
260              
261 4         12 my $digest_type = delete $config->{password_digest}{type};
262 4         8 my $digest = eval {
263 4         9 Digest->new( $digest_type, %{ $config->{password_digest} } )
  4         34  
264             };
265 4 100       1577 if ( my $error = $@ ) {
266 2 100       50 if ( $error =~ m{Can't locate Digest/${digest_type}\.pm in \@INC} ) {
267 1         13 die sprintf(
268             q{Error configuring Auth::Basic plugin: Password digest type "%s" not found}."\n",
269             $digest_type,
270             );
271             }
272 1         10 die "Error configuring Auth::Basic plugin: Error loading Digest module: $@\n";
273             }
274              
275             $app->yancy->filter->add( 'auth.digest' => sub {
276 1     1   4 my ( $name, $value, $field ) = @_;
277 1         22 return $digest->add( $value )->b64digest;
278 2         11 } );
279              
280             # Add the password filter so editing passwords in the editor works
281 2         30 my $schema = $app->yancy->schema( $coll );
282 2         14 push @{ $schema->{properties}{$password_field}{'x-filter'} }, 'auth.digest';
  2         11  
283 2         10 $app->yancy->schema( $coll, $schema );
284              
285             # Add login pages
286 2   66     16 my $route = $config->{route} || $app->yancy->editor->route;
287 2         9 push @{ $app->renderer->classes }, __PACKAGE__;
  2         9  
288 2         26 $route->get( '/login', \&_get_login, 'yancy.login_form' );
289 2         672 $route->post( '/login', \&_post_login, 'yancy.check_login' );
290 2         577 $self->logout_route(
291             $route->get( '/logout', \&_get_logout, 'yancy.logout' )
292             );
293              
294             # Add authentication check
295             my $auth_route = $route->under( sub {
296 21     21   120452 my ( $c ) = @_;
297             # Check auth
298 21 100       119 return 1 if $c->yancy->auth->current_user;
299             # Render some unauthorized result
300 12 100       2343 if ( grep { $_ eq 'api' } @{ $c->req->url->path } ) {
  26         459  
  12         35  
301             # Render JSON unauthorized response
302 7         68 $c->render(
303             handler => 'openapi', # XXX This started being necessary after Mojolicious::Plugin::OpenAPI 2.0
304             status => 401,
305             openapi => {
306             message => 'You are not authorized to view this page. Please log in.',
307             errors => [],
308             },
309             );
310 7         7464 return;
311             }
312             else {
313             # Render HTML response
314 5         52 $c->render( status => 401, template => 'yancy/auth/unauthorized', logout_route => $self->logout_route );
315 5         74633 return;
316             }
317 2         605 } );
318 2         360 my @routes = @{ $route->children }; # Loop over copy while we modify original
  2         6  
319 2         15 for my $r ( @routes ) {
320 13 100       405 next if $r eq $auth_route; # Can't reparent ourselves or route disappears
321              
322             # Don't add auth to unauthed routes. We need to add the plugin's
323             # routes first so that they are picked up before the `under` we
324             # created, but now we're going back to add auth to all
325             # previously-created routes, so we need to skip the ones that
326             # must be visited by unauthed users.
327 11 100       19 next if grep { $r->name eq $_ } qw( yancy.login_form yancy.check_login yancy.logout );
  33         139  
328              
329 5         34 $auth_route->add_child( $r );
330             }
331 2     1   15 $app->helper( 'yancy.auth.route' => sub { $auth_route } );
  1         3172  
332              
333             # Add auth helpers
334 2         5445 $app->helper( 'yancy.auth.login_form' => currym( $self, 'login_form' ) );
335             $app->helper( 'yancy.auth.get_user' => sub {
336 16     16   585 my ( $c, $username ) = @_;
337             return $username_field
338 16 50       65 ? $c->yancy->backend->list( $coll, { $username_field => $username }, { limit => 1 } )->{items}[0]
339             : $c->yancy->backend->get( $coll, $username );
340 2         5792 } );
341             $app->helper( 'yancy.auth.current_user' => sub {
342 28     28   24390 my ( $c, $username ) = @_;
343 28 100       83 if ( $username ) {
344 2         12 $c->session( username => $username );
345             }
346 28 100       909 return if !$c->session( 'username' );
347 11         4608 return $c->yancy->auth->get_user( $c->session( 'username' ) );
348 2         5477 } );
349             $app->helper( 'yancy.auth.check' => sub {
350 5     5   11231 my ( $c, $username, $pass ) = @_;
351 5         37 my $user = $c->yancy->auth->get_user( $username );
352 5 100       22 if ( !$user ) {
353 1         5 $c->app->log->error(
354             sprintf 'Auth failed: User "%s" does not exist.', $username,
355             );
356 1         27 return;
357             }
358 4 100       18 if ( !$user->{ $password_field } ) {
359 1         5 $c->app->log->error(
360             sprintf 'Auth failed: User "%s" password field "%s" is empty.',
361             $username, $password_field,
362             );
363 1         29 return;
364             }
365 3         46 my $check_pass = $digest->add( $pass )->b64digest;
366 3 100       15 if ( $user->{ $password_field } ne $check_pass ) {
367 1         5 $c->app->log->error(
368             sprintf 'Auth failed: User "%s" password is incorrect (field "%s").',
369             $username, $password_field,
370             );
371 1         32 return;
372             }
373 2         13 return 1;
374 2         5661 } );
375             $app->helper( 'yancy.auth.clear' => sub {
376 1     1   24 my ( $c ) = @_;
377 1         5 delete $c->session->{ username };
378 2         5807 } );
379              
380             }
381              
382             sub login_form {
383 5     5 0 18 my ( $self, $c ) = @_;
384 5         19 return $c->render_to_string( 'yancy/auth/basic/login' );
385             }
386              
387             sub _get_login {
388 2     2   10520 my ( $c ) = @_;
389 2         10 return $c->render( 'yancy/auth/basic/login',
390             return_to => $c->req->headers->referrer,
391             );
392             }
393              
394             sub _post_login {
395 3     3   6455 my ( $c ) = @_;
396 3         16 my $user = $c->param( 'username' );
397 3         1383 my $pass = $c->param( 'password' );
398 3 100       209 if ( $c->yancy->auth->check( $user, $pass ) ) {
399 2         13 $c->yancy->auth->current_user( $user );
400 2   66     16 my $to = $c->req->param( 'return_to' ) // $c->url_for( 'yancy.index' );
401 2         670 $c->res->headers->location( $to );
402 2         58 return $c->rendered( 303 );
403             }
404 1         13 $c->flash( error => 'Username or password incorrect' );
405 1         247 return $c->render( 'yancy/auth/basic/login',
406             status => 400,
407             user => $user,
408             return_to => $c->req->param( 'return_to' ),
409             login_failed => 1,
410             );
411             }
412              
413             sub _get_logout {
414 1     1   126 my ( $c ) = @_;
415 1         5 $c->yancy->auth->clear;
416 1         478 $c->flash( info => 'Logged out' );
417 1         50 return $c->render( 'yancy/auth/basic/login' );
418             }
419              
420             1;
421              
422             =pod
423              
424             =head1 NAME
425              
426             Yancy::Plugin::Auth::Basic - (DEPRECATED) A simple auth module for a site
427              
428             =head1 VERSION
429              
430             version 1.087
431              
432             =head1 SYNOPSIS
433              
434             use Mojolicious::Lite;
435             plugin Yancy => {
436             backend => 'pg://localhost/mysite',
437             schema => {
438             users => {
439             required => [ 'username', 'password' ],
440             properties => {
441             id => { type => 'integer', readOnly => 1 },
442             username => { type => 'string' },
443             password => { type => 'string', format => 'password' },
444             },
445             },
446             },
447             };
448             app->yancy->plugin( 'Auth::Basic' => {
449             schema => 'users',
450             username_field => 'username',
451             password_field => 'password',
452             password_digest => {
453             type => 'SHA-1',
454             },
455             } );
456              
457             =head1 DESCRIPTION
458              
459             B: This plugin is deprecated and will be removed in Yancy v2.000. Please
460             switch to the new pluggable auth L or the new password
461             auth L.
462              
463             This plugin provides a basic authentication and authorization scheme for
464             a L site using L. If a user is authenticated, they are
465             then authorized to use the administration application and API.
466              
467             =encoding utf8
468              
469             =head1 CONFIGURATION
470              
471             This plugin has the following configuration options.
472              
473             =over
474              
475             =item schema
476              
477             The name of the Yancy schema that holds users. Required.
478              
479             =item username_field
480              
481             The name of the field in the schema which is the user's identifier.
482             This can be a user name, ID, or e-mail address, and is provided by the
483             user during login.
484              
485             This field is optional. If not specified, the schema's ID field will
486             be used. For example, if the schema uses the C field as
487             a unique identifier, we don't need to provide a C.
488              
489             plugin Yancy => {
490             schema => {
491             users => {
492             'x-id-field' => 'username',
493             properties => {
494             username => { type => 'string' },
495             password => { type => 'string' },
496             },
497             },
498             },
499             };
500             app->yancy->plugin( 'Auth::Basic' => {
501             schema => 'users',
502             password_digest => { type => 'SHA-1' },
503             } );
504              
505             =item password_field
506              
507             The name of the field to use for the user's password. Defaults to C.
508              
509             This field will automatically be set up to use the L filter to
510             properly hash the password when updating it.
511              
512             =item password_digest
513              
514             This is the hashing mechanism that should be used for passwords. There is no
515             default, so you must configure one.
516              
517             This value should be a hash of digest configuration. The one required
518             field is C, and should be a type supported by the L module:
519              
520             =over
521              
522             =item * MD5 (part of core Perl)
523              
524             =item * SHA-1 (part of core Perl)
525              
526             =item * SHA-256 (part of core Perl)
527              
528             =item * SHA-512 (part of core Perl)
529              
530             =item * Bcrypt (recommended)
531              
532             =back
533              
534             Additional fields are given as configuration to the L module.
535             Not all Digest types require additional configuration.
536              
537             # Use Bcrypt for passwords
538             # Install the Digest::Bcrypt module first!
539             app->yancy->plugin( 'Auth::Basic' => {
540             password_digest => {
541             type => 'Bcrypt',
542             cost => 12,
543             salt => 'abcdefgh♥stuff',
544             },
545             } );
546              
547             =item route
548              
549             The root route that this auth module should protect. Defaults to
550             protecting only the Yancy editor application.
551              
552             =back
553              
554             =head2 Sessions
555              
556             This module uses L
557             sessions|https://mojolicious.org/perldoc/Mojolicious/Controller#session>
558             to store the login information in a secure, signed cookie.
559              
560             To configure the default expiration of a session, use
561             L
562             default_expiration|https://mojolicious.org/perldoc/Mojolicious/Sessions#default_expiration>.
563              
564             use Mojolicious::Lite;
565             # Expire a session after 1 day of inactivity
566             app->sessions->default_expiration( 24 * 60 * 60 );
567              
568             =head1 TEMPLATES
569              
570             To override these templates in your application, provide your own
571             template with the same name.
572              
573             =over
574              
575             =item yancy/auth/login.html.ep
576              
577             This template displays the login form. The form should have two fields,
578             C and C, and perform a C request to C<<
579             url_for 'yancy.check_login' >>
580              
581             =item yancy/auth/unauthorized.html.ep
582              
583             This template displays an error message that the user is not authorized
584             to view this page. This most-often appears when the user is not logged in.
585              
586             =item layouts/yancy/auth.html.ep
587              
588             The layout that Yancy uses when displaying the login form, the
589             unauthorized error message, and other auth-related pages.
590              
591             =back
592              
593             =head1 FILTERS
594              
595             This module provides the following filters. See L
596             Configuration> for how to use filters.
597              
598             =head2 auth.digest
599              
600             Run the field value through the configured password L object and
601             store the Base64-encoded result instead.
602              
603             =head1 HELPERS
604              
605             This plugin adds the following Mojolicious helpers:
606              
607             =head2 yancy.auth.route
608              
609             The L that requires
610             authentication. Add your own routes as children of this route to
611             require authentication for your own routes.
612              
613             my $auth_route = $app->yancy->auth->route;
614             $auth_route->get( '/', sub {
615             my ( $c ) = @_;
616             return $c->render(
617             data => 'You are authorized to view this page',
618             );
619             } );
620              
621             =head2 yancy.auth.current_user
622              
623             Get/set the currently logged-in user. Returns C if no user is
624             logged-in.
625              
626             my $user = $c->yancy->auth->current_user
627             || return $c->render( status => 401, text => 'Unauthorized' );
628              
629             To set the current user, pass in the username.
630              
631             $c->yancy->auth->current_user( $username );
632              
633             =head2 yancy.auth.get_user
634              
635             my $user = $c->yancy->auth->get_user( $username );
636              
637             Get a user item by its C.
638              
639             =head2 yancy.auth.check
640              
641             Check a username and password to authenticate a user. Returns true
642             if the user is authenticated, or returns false.
643              
644             B: Does not change the currently logged-in user.
645              
646             if ( $c->yancy->auth->check( $username, $password ) ) {
647             # Authentication succeeded
648             $c->yancy->auth->current_user( $username );
649             }
650              
651             =head2 yancy.auth.clear
652              
653             Clear the currently logged-in user (logout).
654              
655             $c->yancy->auth->clear;
656              
657             =head1 SEE ALSO
658              
659             L, L
660              
661             =head1 AUTHOR
662              
663             Doug Bell
664              
665             =head1 COPYRIGHT AND LICENSE
666              
667             This software is copyright (c) 2021 by Doug Bell.
668              
669             This is free software; you can redistribute it and/or modify it under
670             the same terms as the Perl 5 programming language system itself.
671              
672             =cut
673              
674             __DATA__