File Coverage

blib/lib/Yancy/Plugin/Auth/Password.pm
Criterion Covered Total %
statement 170 188 90.4
branch 47 64 73.4
condition 21 38 55.2
subroutine 22 22 100.0
pod 0 4 0.0
total 260 316 82.2


line stmt bran cond sub pod time code
1             package Yancy::Plugin::Auth::Password;
2             our $VERSION = '1.086';
3             # ABSTRACT: A simple password-based auth
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 => 'sqlite://myapp.db',
12             #pod schema => {
13             #pod users => {
14             #pod 'x-id-field' => 'username',
15             #pod properties => {
16             #pod username => { type => 'string' },
17             #pod password => { type => 'string', format => 'password' },
18             #pod },
19             #pod },
20             #pod },
21             #pod };
22             #pod app->yancy->plugin( 'Auth::Password' => {
23             #pod schema => 'users',
24             #pod username_field => 'username',
25             #pod password_field => 'password',
26             #pod password_digest => {
27             #pod type => 'SHA-1',
28             #pod },
29             #pod } );
30             #pod
31             #pod =head1 DESCRIPTION
32             #pod
33             #pod B This module is C and its API may change before
34             #pod Yancy v2.000 is released.
35             #pod
36             #pod This plugin provides a basic password-based authentication scheme for
37             #pod a site.
38             #pod
39             #pod This module composes the L role
40             #pod to provide the
41             #pod L
42             #pod authorization method.
43             #pod
44             #pod =head2 Adding Users
45             #pod
46             #pod To add the initial user (or any user), use the L
47             #pod command|Mojolicious::Command::eval>:
48             #pod
49             #pod ./myapp.pl eval 'yancy->create( users => { username => "dbell", password => "123qwe" } )'
50             #pod
51             #pod This plugin adds the L filter to the C field, so
52             #pod the password passed to C<< yancy->create >> will be properly hashed in
53             #pod the database.
54             #pod
55             #pod You can use this same technique to edit users from the command-line if
56             #pod someone gets locked out:
57             #pod
58             #pod ./myapp.pl eval 'yancy->update( users => "dbell", { password => "123qwe" } )'
59             #pod
60             #pod =head2 Migrate from Auth::Basic
61             #pod
62             #pod To migrate from the deprecated L module, you
63             #pod should set the C config setting to the C
64             #pod settings from your Auth::Basic configuration. If they are the same as your
65             #pod current password digest settings, you don't need to do anything at all.
66             #pod
67             #pod # Migrate from Auth::Basic, which had SHA-1 passwords, to
68             #pod # Auth::Password using SHA-256 passwords
69             #pod app->yancy->plugin( 'Auth::Password' => {
70             #pod schema => 'users',
71             #pod username_field => 'username',
72             #pod password_field => 'password',
73             #pod migrate_digest => {
74             #pod type => 'SHA-1',
75             #pod },
76             #pod password_digest => {
77             #pod type => 'SHA-256',
78             #pod },
79             #pod } );
80             #pod
81             #pod # Migrate from Auth::Basic, which had SHA-1 passwords, to
82             #pod # Auth::Password using SHA-1 passwords
83             #pod app->yancy->plugin( 'Auth::Password' => {
84             #pod schema => 'users',
85             #pod username_field => 'username',
86             #pod password_field => 'password',
87             #pod password_digest => {
88             #pod type => 'SHA-1',
89             #pod },
90             #pod } );
91             #pod
92             #pod =head2 Verifying Yancy Passwords in SQL (or other languages)
93             #pod
94             #pod Passwords are stored as base64. The Perl L module's removes the
95             #pod trailing padding from a base64 string. This means that if you try to use
96             #pod another method to verify passwords, you must also remove any trailing
97             #pod C<=> from the base64 hash you generate.
98             #pod
99             #pod Here are some examples for how to generate the same password hash in
100             #pod different databases/languages:
101             #pod
102             #pod * Perl:
103             #pod
104             #pod $ perl -MDigest -E'say Digest->new( "SHA-1" )->add( "password" )->b64digest'
105             #pod W6ph5Mm5Pz8GgiULbPgzG37mj9g
106             #pod
107             #pod * MySQL:
108             #pod
109             #pod mysql> SELECT TRIM( TRAILING "=" FROM TO_BASE64( UNHEX( SHA1( "password" ) ) ) );
110             #pod +--------------------------------------------------------------------+
111             #pod | TRIM( TRAILING "=" FROM TO_BASE64( UNHEX( SHA1( "password" ) ) ) ) |
112             #pod +--------------------------------------------------------------------+
113             #pod | W6ph5Mm5Pz8GgiULbPgzG37mj9g |
114             #pod +--------------------------------------------------------------------+
115             #pod
116             #pod * Postgres:
117             #pod
118             #pod yancy=# CREATE EXTENSION pgcrypto;
119             #pod CREATE EXTENSION
120             #pod yancy=# SELECT TRIM( TRAILING '=' FROM encode( digest( 'password', 'sha1' ), 'base64' ) );
121             #pod rtrim
122             #pod -----------------------------
123             #pod W6ph5Mm5Pz8GgiULbPgzG37mj9g
124             #pod (1 row)
125             #pod
126             #pod =head1 CONFIGURATION
127             #pod
128             #pod This plugin has the following configuration options.
129             #pod
130             #pod =head2 schema
131             #pod
132             #pod The name of the Yancy schema that holds users. Required.
133             #pod
134             #pod =head2 username_field
135             #pod
136             #pod The name of the field in the schema which is the user's identifier.
137             #pod This can be a user name, ID, or e-mail address, and is provided by the
138             #pod user during login.
139             #pod
140             #pod This field is optional. If not specified, the schema's ID field will
141             #pod be used. For example, if the schema uses the C field as
142             #pod a unique identifier, we don't need to provide a C.
143             #pod
144             #pod plugin Yancy => {
145             #pod schema => {
146             #pod users => {
147             #pod 'x-id-field' => 'username',
148             #pod properties => {
149             #pod username => { type => 'string' },
150             #pod password => { type => 'string' },
151             #pod },
152             #pod },
153             #pod },
154             #pod };
155             #pod app->yancy->plugin( 'Auth::Password' => {
156             #pod schema => 'users',
157             #pod password_digest => { type => 'SHA-1' },
158             #pod } );
159             #pod
160             #pod =head2 password_field
161             #pod
162             #pod The name of the field to use for the password. Defaults to C.
163             #pod
164             #pod This field will automatically be set up to use the L filter to
165             #pod properly hash the password when updating it.
166             #pod
167             #pod =head2 password_digest
168             #pod
169             #pod This is the hashing mechanism that should be used for hashing passwords.
170             #pod This value should be a hash of digest configuration. The one required
171             #pod field is C, and should be a type supported by the L module:
172             #pod
173             #pod =over
174             #pod
175             #pod =item * MD5 (part of core Perl)
176             #pod
177             #pod =item * SHA-1 (part of core Perl)
178             #pod
179             #pod =item * SHA-256 (part of core Perl)
180             #pod
181             #pod =item * SHA-512 (part of core Perl)
182             #pod
183             #pod =item * Bcrypt (recommended)
184             #pod
185             #pod =back
186             #pod
187             #pod Additional fields are given as configuration to the L module.
188             #pod Not all Digest types require additional configuration.
189             #pod
190             #pod There is no default: Perl core provides SHA-1 hashes, but those aren't good
191             #pod enough. We recommend installing L for password hashing.
192             #pod
193             #pod # Use Bcrypt for passwords
194             #pod # Install the Digest::Bcrypt module first!
195             #pod app->yancy->plugin( 'Auth::Basic' => {
196             #pod password_digest => {
197             #pod type => 'Bcrypt',
198             #pod cost => 12,
199             #pod salt => 'abcdefgh♥stuff',
200             #pod },
201             #pod } );
202             #pod
203             #pod Digest information is stored with the password so that password digests
204             #pod can be updated transparently when necessary. Changing the digest
205             #pod configuration will result in a user's password being upgraded the next
206             #pod time they log in.
207             #pod
208             #pod =head2 allow_register
209             #pod
210             #pod If true, allow the visitor to register their own user account.
211             #pod
212             #pod =head2 register_fields
213             #pod
214             #pod An array of fields to show to the user when registering an account. By
215             #pod default, all required fields from the schema will be presented in the
216             #pod form to register.
217             #pod
218             #pod =head2 Sessions
219             #pod
220             #pod This module uses L
221             #pod sessions|https://mojolicious.org/perldoc/Mojolicious/Controller#session>
222             #pod to store the login information in a secure, signed cookie.
223             #pod
224             #pod To configure the default expiration of a session, use
225             #pod L
226             #pod default_expiration|https://mojolicious.org/perldoc/Mojolicious/Sessions#default_expiration>.
227             #pod
228             #pod use Mojolicious::Lite;
229             #pod # Expire a session after 1 day of inactivity
230             #pod app->sessions->default_expiration( 24 * 60 * 60 );
231             #pod
232             #pod =head1 FILTERS
233             #pod
234             #pod This module provides the following filters. See L
235             #pod Configuration> for how to use filters.
236             #pod
237             #pod =head2 auth.digest
238             #pod
239             #pod Run the field value through the configured password L object and
240             #pod store the Base64-encoded result instead.
241             #pod
242             #pod =head1 HELPERS
243             #pod
244             #pod This plugin has the following helpers.
245             #pod
246             #pod =head2 yancy.auth.current_user
247             #pod
248             #pod Get the current user from the session, if any. Returns C if no
249             #pod user was found in the session.
250             #pod
251             #pod my $user = $c->yancy->auth->current_user
252             #pod || return $c->render( status => 401, text => 'Unauthorized' );
253             #pod
254             #pod =head2 yancy.auth.require_user
255             #pod
256             #pod Validate there is a logged-in user and optionally that the user data has
257             #pod certain values. See L.
258             #pod
259             #pod # Display the user dashboard, but only to logged-in users
260             #pod my $auth_route = $app->routes->under( '/user', $app->yancy->auth->require_user );
261             #pod $auth_route->get( '' )->to( 'user#dashboard' );
262             #pod
263             #pod =head2 yancy.auth.login_form
264             #pod
265             #pod Return an HTML string containing the rendered login form.
266             #pod
267             #pod %# Display a login form to an unauthenticated visitor
268             #pod % if ( !$c->yancy->auth->current_user ) {
269             #pod %= $c->yancy->auth->login_form
270             #pod % }
271             #pod
272             #pod The login form will create a C hidden field to try to bring
273             #pod the user back to what they were doing when they were asked to log in.
274             #pod This field defaults to the value of the C parameter or the
275             #pod value of the C flash value. If neither is set, it defaults to
276             #pod the HTTP referrer if the form is displayed on the login page (under the
277             #pod URL C) or the current page.
278             #pod
279             #pod # Redirect user to login page, and return them here
280             #pod under sub( $c ) {
281             #pod return 1 if $c->yancy->auth->current_user;
282             #pod $c->flash({ return_to => $c->req->url });
283             #pod $c->redirect_to('yancy.auth.password.login');
284             #pod return undef;
285             #pod }
286             #pod
287             #pod # Build a link to log in and then redirect to the dashboard
288             #pod $c->url_for( 'yancy.auth.password.login' )
289             #pod ->query({ return_to => $c->url_for( 'dashboard' ) });
290             #pod
291             #pod =head2 yancy.auth.logout
292             #pod
293             #pod Log out any current account. Use this in your own controller actions to
294             #pod perform a logout.
295             #pod
296             #pod =head1 ROUTES
297             #pod
298             #pod This plugin creates the following L
299             #pod routes|https://mojolicious.org/perldoc/Mojolicious/Guides/Routing#Named-routes>.
300             #pod Use named routes with helpers like
301             #pod L,
302             #pod L, and
303             #pod L.
304             #pod
305             #pod =head2 yancy.auth.password.login_form
306             #pod
307             #pod Display the login form using the L template. This route handles C
308             #pod requests and can be used with the L,
309             #pod L,
310             #pod and L helpers.
311             #pod
312             #pod %= link_to Login => 'yancy.auth.password.login_form'
313             #pod <%= link_to 'yancy.auth.password.login_form', begin %>Login<% end %>
314             #pod

Login here: <%= url_for 'yancy.auth.password.login_form' %>

315             #pod
316             #pod =head2 yancy.auth.password.login
317             #pod
318             #pod Handle login by checking the user's username and password. This route
319             #pod handles C requests and can be used with the
320             #pod L
321             #pod and L
322             #pod helpers.
323             #pod
324             #pod %= form_for 'yancy.auth.password.login' => begin
325             #pod %= text_field 'username', placeholder => 'Username'
326             #pod %= text_field 'password', placeholder => 'Password'
327             #pod %= submit_button
328             #pod % end
329             #pod
330             #pod =head2 yancy.auth.password.logout
331             #pod
332             #pod Clear the current login and allow the user to log in again. This route handles C
333             #pod requests and can be used with the L,
334             #pod L,
335             #pod and L helpers.
336             #pod
337             #pod %= link_to Logout => 'yancy.auth.password.logout'
338             #pod <%= link_to 'yancy.auth.password.logout', begin %>Logout<% end %>
339             #pod

Logout here: <%= url_for 'yancy.auth.password.logout' %>

340             #pod
341             #pod =head2 yancy.auth.password.register_form
342             #pod
343             #pod Display the form to register a new user, if registration is enabled. This route handles C
344             #pod requests and can be used with the L,
345             #pod L,
346             #pod and L helpers.
347             #pod
348             #pod %= link_to Register => 'yancy.auth.password.register_form'
349             #pod <%= link_to 'yancy.auth.password.register_form', begin %>Register<% end %>
350             #pod

Register here: <%= url_for 'yancy.auth.password.register_form' %>

351             #pod
352             #pod =head2 yancy.auth.password.register
353             #pod
354             #pod Register a new user, if registration is enabled. This route
355             #pod handles C requests and can be used with the
356             #pod L
357             #pod and L
358             #pod helpers.
359             #pod
360             #pod %= form_for 'yancy.auth.password.register' => begin
361             #pod %= text_field 'username', placeholder => 'Username'
362             #pod %= text_field 'password', placeholder => 'Password'
363             #pod %= text_field 'password-verify', placeholder => 'Password (again)'
364             #pod %# ... Display other fields required for registration
365             #pod %= submit_button
366             #pod % end
367             #pod
368             #pod =head1 TEMPLATES
369             #pod
370             #pod To override these templates, add your own at the designated path inside
371             #pod your app's C directory.
372             #pod
373             #pod =head2 yancy/auth/password/login_form.html.ep
374             #pod
375             #pod The form to log in.
376             #pod
377             #pod =head2 yancy/auth/password/login_page.html.ep
378             #pod
379             #pod The page containing the form to log in. Uses the C
380             #pod template for the form itself.
381             #pod
382             #pod =head2 yancy/auth/unauthorized.html.ep
383             #pod
384             #pod This template displays an error message that the user is not authorized
385             #pod to view this page. This most-often appears when the user is not logged
386             #pod in.
387             #pod
388             #pod =head2 yancy/auth/unauthorized.json.ep
389             #pod
390             #pod This template renders a JSON object with an "errors" array explaining
391             #pod the error.
392             #pod
393             #pod =head2 layouts/yancy/auth.html.ep
394             #pod
395             #pod The layout that Yancy uses when displaying the login form, the
396             #pod unauthorized error message, and other auth-related pages.
397             #pod
398             #pod =head2 yancy/auth/password/register.html.ep
399             #pod
400             #pod The page containing the form to register a new user. Will display all of the
401             #pod L.
402             #pod
403             #pod =head1 SEE ALSO
404             #pod
405             #pod L
406             #pod
407             #pod =cut
408              
409 5     5   5116 use Mojo::Base 'Mojolicious::Plugin';
  5         13  
  5         46  
410 5     5   2616 use Role::Tiny::With;
  5         1048  
  5         342  
411             with 'Yancy::Plugin::Auth::Role::RequireUser';
412 5     5   34 use Yancy::Util qw( currym derp );
  5         11  
  5         290  
413 5     5   31 use Digest;
  5         10  
  5         18338  
414              
415             has log =>;
416             has schema =>;
417             has username_field =>;
418             has password_field => 'password';
419             has allow_register => 0;
420             has plugin_field => undef;
421             has register_fields => sub { [] };
422             has moniker => 'password';
423             has default_digest =>;
424             has route =>;
425             has logout_route =>;
426              
427             # The Auth::Basic digest configuration to migrate from. Auth::Basic did
428             # not store the digest information in the password, so we need to fix
429             # it.
430             has migrate_digest =>;
431              
432             sub register {
433             my ( $self, $app, $config ) = @_;
434             $self->init( $app, $config );
435             $app->helper(
436             'yancy.auth.current_user' => currym( $self, 'current_user' ),
437             );
438             $app->helper(
439             'yancy.auth.logout' => currym( $self, 'logout' ),
440             );
441             $app->helper(
442             'yancy.auth.login_form' => currym( $self, 'login_form' ),
443             );
444             }
445              
446             sub init {
447 11     11 0 36 my ( $self, $app, $config ) = @_;
448             my $schema_name = $config->{schema} || $config->{collection}
449 11   0     70 || die "Error configuring Auth::Password plugin: No schema defined\n";
450             derp "'collection' configuration in Auth::Token is now 'schema'. Please fix your configuration.\n"
451 11 50       45 if $config->{collection};
452 11         57 my $schema = $app->yancy->schema( $schema_name );
453 11 50       82 die sprintf(
454             q{Error configuring Auth::Password plugin: Collection "%s" not found}."\n",
455             $schema_name,
456             ) unless $schema;
457              
458 11         78 $self->log( $app->log );
459 11         209 $self->schema( $schema_name );
460 11         113 $self->username_field( $config->{username_field} );
461 11   50     112 $self->password_field( $config->{password_field} || 'password' );
462 11         98 $self->default_digest( $config->{password_digest} );
463 11         102 $self->migrate_digest( $config->{migrate_digest} );
464 11         119 $self->allow_register( $config->{allow_register} );
465             $self->register_fields(
466 11   100     145 $config->{register_fields} || $app->yancy->schema( $schema_name )->{required} || []
467             );
468             $app->yancy->filter->add( 'yancy.plugin.auth.password' => sub {
469 3     3   13 my ( $key, $value, $schema, @params ) = @_;
470 3         16 return $self->_digest_password( $value );
471 11         231 } );
472              
473             # Update the schema to digest the password correctly
474 11         178 my $field = $schema->{properties}{ $self->password_field };
475 11 100       68 if ( !grep { $_ eq 'yancy.plugin.auth.password' } @{ $field->{'x-filter'} || [] } ) {
  1 100       5  
  11         86  
476 10   50     34 push @{ $field->{ 'x-filter' } ||= [] },
  10         71  
477             'yancy.plugin.auth.password';
478             }
479 11         35 $field->{ format } = 'password';
480 11         40 $app->yancy->schema( $schema_name, $schema );
481              
482             # Add fields that may not technically be required by the schema, but
483             # are required for registration
484             my @user_fields = (
485 11   0     68 $self->username_field || $schema->{'x-id-field'} || 'id',
486             $self->password_field,
487             );
488 11         128 for my $field ( @user_fields ) {
489 22 100       43 if ( !grep { $_ eq $field } @{ $self->register_fields } ) {
  61         229  
  22         53  
490 2         6 unshift @{ $self->register_fields }, $field;
  2         5  
491             }
492             }
493              
494 11         54 my $route = $app->yancy->routify( $config->{route}, '/yancy/auth/' . $self->moniker );
495 11         4782 $self->route( $route );
496 11         96 $route->get( 'register' )->to( cb => currym( $self, '_get_register' ) )->name( 'yancy.auth.password.register_form' );
497 11         494 $route->post( 'register' )->to( cb => currym( $self, '_post_register' ) )->name( 'yancy.auth.password.register' );
498 11         369 $self->logout_route(
499             $route->get( 'logout' )->to( cb => currym( $self, '_get_logout' ) )->name( 'yancy.auth.password.logout' )
500             );
501 11         444 $route->get( '' )->to( cb => currym( $self, '_get_login' ) )->name( 'yancy.auth.password.login_form' );
502 11         410 $route->post( '' )->to( cb => currym( $self, '_post_login' ) )->name( 'yancy.auth.password.login' );
503             }
504              
505             sub _get_user {
506 34     34   113 my ( $self, $c, $username ) = @_;
507 34         135 my $schema_name = $self->schema;
508 34         220 my $username_field = $self->username_field;
509 34         155 my %search;
510 34 100       103 if ( my $field = $self->plugin_field ) {
511 3         25 $search{ $field } = $self->moniker;
512             }
513 34 50       213 if ( $username_field ) {
514 34         102 $search{ $username_field } = $username;
515 34         59 my ( $user ) = @{ $c->yancy->backend->list( $schema_name, \%search, { limit => 1 } )->{items} };
  34         136  
516 34         199 return $user;
517             }
518 0         0 return $c->yancy->backend->get( $schema_name, $username );
519             }
520              
521             sub _digest_password {
522 4     4   10 my ( $self, $password ) = @_;
523 4         14 my $config = $self->default_digest;
524 4         28 my $digest_config_string = _build_digest_config_string( $config );
525 4         14 my $digest = _get_digest_by_config_string( $digest_config_string );
526 4         49 my $password_string = join '$', $digest->add( $password )->b64digest, $digest_config_string;
527 4         44 return $password_string;
528             }
529              
530             sub _set_password {
531 1     1   5 my ( $self, $c, $username, $password ) = @_;
532 1         3 my $password_string = eval { $self->_digest_password( $password ) };
  1         4  
533 1 50       6 if ( $@ ) {
534 0         0 $self->log->error(
535             sprintf 'Error setting password for user "%s": %s', $username, $@,
536             );
537             }
538              
539 1         6 my $id = $self->_get_id_for_username( $c, $username );
540 1         6 $c->yancy->backend->set( $self->schema, $id, { $self->password_field => $password_string } );
541             }
542              
543             sub _get_id_for_username {
544 1     1   4 my ( $self, $c, $username ) = @_;
545 1         4 my $schema_name = $self->schema;
546 1         8 my $schema = $c->yancy->schema( $schema_name );
547 1         5 my $id = $username;
548 1   50     6 my $id_field = $schema->{'x-id-field'} || 'id';
549 1         4 my $username_field = $self->username_field;
550 1 50 33     19 if ( $username_field && $username_field ne $id_field ) {
551 0         0 $id = $self->_get_user( $c, $username )->{ $id_field };
552             }
553 1         13 return $id;
554             }
555              
556             sub current_user {
557 35     35 0 98 my ( $self, $c ) = @_;
558 35 50       107 return undef unless my $session = $c->session;
559 35 100       8415 my $yancy = $session->{yancy} or return undef;
560 18 100       69 my $auth = $yancy->{auth} or return undef;
561 17 50       57 my $username = $auth->{password} or return undef;
562 17         62 my $user = $self->_get_user( $c, $username );
563 17         80 delete $user->{ $self->password_field };
564 17         156 return $user;
565             }
566              
567             sub login_form {
568 17     17 0 39060 my ( $self, $c ) = @_;
569 17 100 66     67 my $return_to
    100 50        
    100          
570             # If we've specified one, go there directly
571             = $c->req->param( 'return_to' )
572             ? $c->req->param( 'return_to' )
573             # Check flash storage, perhaps from a redirect to the login form
574             : $c->flash('return_to') ? $c->flash('return_to')
575             # If this is the login page, go back to referer
576             : $c->current_route =~ /^yancy\.auth/
577             && $c->req->headers->referrer
578             && $c->req->headers->referrer !~ m{^(?:\w+:|//)}
579             ? $c->req->headers->referrer
580             # Otherwise, return the user here
581             : ( $c->req->url->path || '/' )
582             ;
583 17 50       5613 if ( $return_to =~ m{^(?:\w+:|//)} ) {
584 0         0 return $c->reply->exception(
585             q{`return_to` can not contain URL scheme or host},
586             );
587             }
588 17         1011 return $c->render_to_string(
589             'yancy/auth/password/login_form',
590             plugin => $self,
591             return_to => $return_to,
592             );
593             }
594              
595             sub _get_login {
596 4     4   18 my ( $self, $c ) = @_;
597 4         20 return $c->render( 'yancy/auth/password/login_page',
598             plugin => $self,
599             );
600             }
601              
602             sub _post_login {
603 14     14   47 my ( $self, $c ) = @_;
604 14         65 my $user = $c->param( 'username' );
605 14         5921 my $pass = $c->param( 'password' );
606 14 100       920 if ( $self->_check_pass( $c, $user, $pass ) ) {
607 11         60 $c->session->{yancy}{auth}{password} = $user;
608 11   100     4712 my $to = $c->req->param( 'return_to' ) || '/';
609              
610             # Do not allow return_to to redirect the user to another site.
611             # http://cwe.mitre.org/data/definitions/601.html
612 11 100       513 if ( $to =~ m{^(?:\w+:|//)} ) {
613 2         18 return $c->reply->exception(
614             q{`return_to` can not contain URL scheme or host},
615             );
616             }
617              
618 9         38 $c->res->headers->location( $to );
619 9         274 return $c->rendered( 303 );
620             }
621 3         30 $c->flash( error => 'Username or password incorrect' );
622 3         511 return $c->render( 'yancy/auth/password/login_page',
623             status => 400,
624             plugin => $self,
625             user => $user,
626             login_failed => 1,
627             );
628             }
629              
630             sub _get_register {
631 1     1   4 my ( $self, $c ) = @_;
632 1 50       5 if ( !$self->allow_register ) {
633 0         0 $c->app->log->error( 'Registration not allowed (set allow_register)' );
634 0         0 $c->reply->not_found;
635 0         0 return;
636             }
637 1         13 return $c->render( 'yancy/auth/password/register',
638             plugin => $self,
639             );
640             }
641              
642             sub _post_register {
643 4     4   16 my ( $self, $c ) = @_;
644 4 50       20 if ( !$self->allow_register ) {
645 0         0 $c->app->log->error( 'Registration not allowed (set allow_register)' );
646 0         0 $c->reply->not_found;
647 0         0 return;
648             }
649              
650 4         39 my $schema_name = $self->schema;
651 4         42 my $schema = $c->yancy->schema( $schema_name );
652 4   0     23 my $username_field = $self->username_field || $schema->{'x-id-field'} || 'id';
653 4         40 my $password_field = $self->password_field;
654              
655 4         36 my $username = $c->param( $username_field );
656 4         1973 my $pass = $c->param( $self->password_field );
657 4 100       307 if ( $pass ne $c->param( $self->password_field . '-verify' ) ) {
658 1         71 return $c->render( 'yancy/auth/password/register',
659             status => 400,
660             plugin => $self,
661             user => $username,
662             error => 'password_verify',
663             );
664             }
665 3 100       218 if ( $self->_get_user( $c, $username ) ) {
666 1         8 return $c->render( 'yancy/auth/password/register',
667             status => 400,
668             plugin => $self,
669             user => $username,
670             error => 'user_exists',
671             );
672             }
673              
674             # Create new user
675             my $item = {
676             $username_field => $username,
677             $password_field => $pass,
678             (
679 2         11 map { $_ => $c->param( $_ ) }
680 6         85 grep { !/^(?:$username_field|$password_field)$/ }
681 2         8 @{ $self->register_fields }
  2         9  
682             ),
683             };
684 2         149 my $id = eval { $c->yancy->create( $schema_name, $item ) };
  2         10  
685 2 100       31 if ( my $exception = $@ ) {
686 1 50       6 my $error = ref $exception eq 'ARRAY' ? 'validation' : 'create';
687 1         7 $c->app->log->error( 'Error creating user: ' . $exception );
688 1         23 return $c->render( 'yancy/auth/password/register',
689             status => 400,
690             plugin => $self,
691             user => $username,
692             error => $error,
693             exception => $exception,
694             );
695             }
696              
697             # Get them to log in
698 1         9 $c->flash( info => 'user_created' );
699 1         615 return $c->redirect_to( 'yancy.auth.password.login' );
700             }
701              
702             sub _get_digest {
703 17     17   46 my ( $type, @config ) = @_;
704 17         35 my $digest = eval {
705 17         137 Digest->new( $type, @config )
706             };
707 17 50       847 if ( my $error = $@ ) {
708 0 0       0 if ( $error =~ m{Can't locate Digest/${type}\.pm in \@INC} ) {
709 0         0 die sprintf q{Password digest type "%s" not found}."\n", $type;
710             }
711 0         0 die sprintf "Error loading Digest module: %s\n", $@;
712             }
713 17         50 return $digest;
714             }
715              
716             sub _get_digest_by_config_string {
717 17     17   45 my ( $config_string ) = @_;
718 17         49 my @digest_parts = split /\$/, $config_string;
719 17         67 return _get_digest( @digest_parts );
720             }
721              
722             sub _build_digest_config_string {
723 17     17   93 my ( $config ) = @_;
724             my @config_parts = (
725 17         119 map { $_, $config->{$_} } grep !/^type$/, keys %$config
  0         0  
726             );
727 17         78 return join '$', $config->{type}, @config_parts;
728             }
729              
730             sub _check_pass {
731 14     14   49 my ( $self, $c, $username, $input_password ) = @_;
732 14         183 my $user = $self->_get_user( $c, $username );
733              
734 14 100       64 if ( !$user ) {
735 1         7 $self->log->error(
736             sprintf 'Error checking password for user "%s": User does not exist',
737             $username
738             );
739 1         24 return undef;
740             }
741              
742             my ( $user_password, $user_digest_config_string )
743 13         62 = split /\$/, $user->{ $self->password_field }, 2;
744              
745 13         127 my $force_upgrade = 0;
746 13 50       40 if ( !$user_digest_config_string ) {
747             # This password must have come from the Auth::Basic module,
748             # which did not have digest configuration stored with the
749             # password. So, we need to know what kind of digest to use, and
750             # we need to fix the password.
751 0   0     0 $user_digest_config_string = _build_digest_config_string(
752             $self->migrate_digest || $self->default_digest
753             );
754 0         0 $force_upgrade = 1;
755             }
756              
757 13         32 my $digest = eval { _get_digest_by_config_string( $user_digest_config_string ) };
  13         44  
758 13 50       41 if ( $@ ) {
759 0         0 die sprintf 'Error checking password for user "%s": %s', $username, $@;
760             }
761 13         157 my $check_password = $digest->add( $input_password )->b64digest;
762              
763 13         67 my $success = $check_password eq $user_password;
764              
765 13         54 my $default_config_string = _build_digest_config_string( $self->default_digest );
766 13 100 66     105 if ( $success && ( $force_upgrade || $user_digest_config_string ne $default_config_string ) ) {
      100        
767             # We need to re-create the user's password field using the new
768             # settings
769 1         7 $self->_set_password( $c, $username, $input_password );
770             }
771              
772 13         122 return $success;
773             }
774              
775             sub logout {
776 6     6 0 23 my ( $self, $c ) = @_;
777 6         24 delete $c->session->{yancy}{auth}{password};
778             }
779              
780             sub _get_logout {
781 3     3   9 my ( $self, $c ) = @_;
782 3         12 $self->logout( $c );
783 3         1522 $c->res->code( 303 );
784 3   100     50 my $redirect_to = $c->param( 'redirect_to' ) // $c->req->headers->referrer // '/';
      100        
785 3 50       858 if ( $redirect_to eq $c->req->url->path ) {
786 0         0 $redirect_to = '/';
787             }
788 3         616 return $c->redirect_to( $redirect_to );
789             }
790              
791             1;
792              
793             __END__