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.088'; | ||||||
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 |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
63 | #pod should set 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 |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
275 | #pod value of the C |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
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 | 6303 | use Mojo::Base 'Mojolicious::Plugin'; | |||
5 | 26 | ||||||
5 | 44 | ||||||
410 | 5 | 5 | 4180 | use Role::Tiny::With; | |||
5 | 1353 | ||||||
5 | 421 | ||||||
411 | with 'Yancy::Plugin::Auth::Role::RequireUser'; | ||||||
412 | 5 | 5 | 43 | use Yancy::Util qw( currym derp ); | |||
5 | 15 | ||||||
5 | 296 | ||||||
413 | 5 | 5 | 36 | use Digest; | |||
5 | 12 | ||||||
5 | 20010 | ||||||
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 | 127 | my ( $self, $app, $config ) = @_; | ||
448 | my $schema_name = $config->{schema} || $config->{collection} | ||||||
449 | 11 | 0 | 96 | || 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 | 53 | if $config->{collection}; | |||
452 | 11 | 59 | my $schema = $app->yancy->schema( $schema_name ); | ||||
453 | 11 | 50 | 87 | die sprintf( | |||
454 | q{Error configuring Auth::Password plugin: Collection "%s" not found}."\n", | ||||||
455 | $schema_name, | ||||||
456 | ) unless $schema; | ||||||
457 | |||||||
458 | 11 | 63 | $self->log( $app->log ); | ||||
459 | 11 | 227 | $self->schema( $schema_name ); | ||||
460 | 11 | 113 | $self->username_field( $config->{username_field} ); | ||||
461 | 11 | 50 | 114 | $self->password_field( $config->{password_field} || 'password' ); | |||
462 | 11 | 109 | $self->default_digest( $config->{password_digest} ); | ||||
463 | 11 | 104 | $self->migrate_digest( $config->{migrate_digest} ); | ||||
464 | 11 | 140 | $self->allow_register( $config->{allow_register} ); | ||||
465 | $self->register_fields( | ||||||
466 | 11 | 100 | 132 | $config->{register_fields} || $app->yancy->schema( $schema_name )->{required} || [] | |||
467 | ); | ||||||
468 | $app->yancy->filter->add( 'yancy.plugin.auth.password' => sub { | ||||||
469 | 3 | 3 | 11 | my ( $key, $value, $schema, @params ) = @_; | |||
470 | 3 | 17 | return $self->_digest_password( $value ); | ||||
471 | 11 | 237 | } ); | ||||
472 | |||||||
473 | # Update the schema to digest the password correctly | ||||||
474 | 11 | 213 | my $field = $schema->{properties}{ $self->password_field }; | ||||
475 | 11 | 100 | 68 | if ( !grep { $_ eq 'yancy.plugin.auth.password' } @{ $field->{'x-filter'} || [] } ) { | |||
1 | 100 | 6 | |||||
11 | 108 | ||||||
476 | 10 | 50 | 24 | push @{ $field->{ 'x-filter' } ||= [] }, | |||
10 | 70 | ||||||
477 | 'yancy.plugin.auth.password'; | ||||||
478 | } | ||||||
479 | 11 | 41 | $field->{ format } = 'password'; | ||||
480 | 11 | 49 | $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 | 70 | $self->username_field || $schema->{'x-id-field'} || 'id', | |||
486 | $self->password_field, | ||||||
487 | ); | ||||||
488 | 11 | 140 | for my $field ( @user_fields ) { | ||||
489 | 22 | 100 | 44 | if ( !grep { $_ eq $field } @{ $self->register_fields } ) { | |||
61 | 226 | ||||||
22 | 53 | ||||||
490 | 2 | 7 | unshift @{ $self->register_fields }, $field; | ||||
2 | 5 | ||||||
491 | } | ||||||
492 | } | ||||||
493 | |||||||
494 | 11 | 50 | my $route = $app->yancy->routify( $config->{route}, '/yancy/auth/' . $self->moniker ); | ||||
495 | 11 | 4905 | $self->route( $route ); | ||||
496 | 11 | 90 | $route->get( 'register' )->to( cb => currym( $self, '_get_register' ) )->name( 'yancy.auth.password.register_form' ); | ||||
497 | 11 | 450 | $route->post( 'register' )->to( cb => currym( $self, '_post_register' ) )->name( 'yancy.auth.password.register' ); | ||||
498 | 11 | 439 | $self->logout_route( | ||||
499 | $route->get( 'logout' )->to( cb => currym( $self, '_get_logout' ) )->name( 'yancy.auth.password.logout' ) | ||||||
500 | ); | ||||||
501 | 11 | 440 | $route->get( '' )->to( cb => currym( $self, '_get_login' ) )->name( 'yancy.auth.password.login_form' ); | ||||
502 | 11 | 360 | $route->post( '' )->to( cb => currym( $self, '_post_login' ) )->name( 'yancy.auth.password.login' ); | ||||
503 | } | ||||||
504 | |||||||
505 | sub _get_user { | ||||||
506 | 34 | 34 | 124 | my ( $self, $c, $username ) = @_; | |||
507 | 34 | 130 | my $schema_name = $self->schema; | ||||
508 | 34 | 222 | my $username_field = $self->username_field; | ||||
509 | 34 | 157 | my %search; | ||||
510 | 34 | 100 | 142 | if ( my $field = $self->plugin_field ) { | |||
511 | 3 | 22 | $search{ $field } = $self->moniker; | ||||
512 | } | ||||||
513 | 34 | 50 | 238 | if ( $username_field ) { | |||
514 | 34 | 115 | $search{ $username_field } = $username; | ||||
515 | 34 | 68 | my ( $user ) = @{ $c->yancy->backend->list( $schema_name, \%search, { limit => 1 } )->{items} }; | ||||
34 | 155 | ||||||
516 | 34 | 207 | return $user; | ||||
517 | } | ||||||
518 | 0 | 0 | return $c->yancy->backend->get( $schema_name, $username ); | ||||
519 | } | ||||||
520 | |||||||
521 | sub _digest_password { | ||||||
522 | 4 | 4 | 15 | my ( $self, $password ) = @_; | |||
523 | 4 | 18 | my $config = $self->default_digest; | ||||
524 | 4 | 28 | my $digest_config_string = _build_digest_config_string( $config ); | ||||
525 | 4 | 18 | my $digest = _get_digest_by_config_string( $digest_config_string ); | ||||
526 | 4 | 65 | my $password_string = join '$', $digest->add( $password )->b64digest, $digest_config_string; | ||||
527 | 4 | 46 | return $password_string; | ||||
528 | } | ||||||
529 | |||||||
530 | sub _set_password { | ||||||
531 | 1 | 1 | 5 | my ( $self, $c, $username, $password ) = @_; | |||
532 | 1 | 4 | my $password_string = eval { $self->_digest_password( $password ) }; | ||||
1 | 5 | ||||||
533 | 1 | 50 | 4 | if ( $@ ) { | |||
534 | 0 | 0 | $self->log->error( | ||||
535 | sprintf 'Error setting password for user "%s": %s', $username, $@, | ||||||
536 | ); | ||||||
537 | } | ||||||
538 | |||||||
539 | 1 | 9 | my $id = $self->_get_id_for_username( $c, $username ); | ||||
540 | 1 | 5 | $c->yancy->backend->set( $self->schema, $id, { $self->password_field => $password_string } ); | ||||
541 | } | ||||||
542 | |||||||
543 | sub _get_id_for_username { | ||||||
544 | 1 | 1 | 5 | my ( $self, $c, $username ) = @_; | |||
545 | 1 | 8 | my $schema_name = $self->schema; | ||||
546 | 1 | 8 | my $schema = $c->yancy->schema( $schema_name ); | ||||
547 | 1 | 3 | my $id = $username; | ||||
548 | 1 | 50 | 4 | my $id_field = $schema->{'x-id-field'} || 'id'; | |||
549 | 1 | 5 | my $username_field = $self->username_field; | ||||
550 | 1 | 50 | 33 | 10 | if ( $username_field && $username_field ne $id_field ) { | ||
551 | 0 | 0 | $id = $self->_get_user( $c, $username )->{ $id_field }; | ||||
552 | } | ||||||
553 | 1 | 14 | return $id; | ||||
554 | } | ||||||
555 | |||||||
556 | sub current_user { | ||||||
557 | 35 | 35 | 0 | 111 | my ( $self, $c ) = @_; | ||
558 | 35 | 50 | 126 | return undef unless my $session = $c->session; | |||
559 | 35 | 100 | 9108 | my $yancy = $session->{yancy} or return undef; | |||
560 | 18 | 100 | 76 | my $auth = $yancy->{auth} or return undef; | |||
561 | 17 | 50 | 60 | my $username = $auth->{password} or return undef; | |||
562 | 17 | 64 | my $user = $self->_get_user( $c, $username ); | ||||
563 | 17 | 77 | delete $user->{ $self->password_field }; | ||||
564 | 17 | 163 | return $user; | ||||
565 | } | ||||||
566 | |||||||
567 | sub login_form { | ||||||
568 | 17 | 17 | 0 | 42990 | my ( $self, $c ) = @_; | ||
569 | 17 | 100 | 66 | 89 | 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 | 5739 | 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 | 1141 | 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 | 16 | my ( $self, $c ) = @_; | |||
597 | 4 | 24 | return $c->render( 'yancy/auth/password/login_page', | ||||
598 | plugin => $self, | ||||||
599 | ); | ||||||
600 | } | ||||||
601 | |||||||
602 | sub _post_login { | ||||||
603 | 14 | 14 | 52 | my ( $self, $c ) = @_; | |||
604 | 14 | 58 | my $user = $c->param( 'username' ); | ||||
605 | 14 | 6211 | my $pass = $c->param( 'password' ); | ||||
606 | 14 | 100 | 959 | if ( $self->_check_pass( $c, $user, $pass ) ) { | |||
607 | 11 | 60 | $c->session->{yancy}{auth}{password} = $user; | ||||
608 | 11 | 100 | 5164 | 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 | 574 | if ( $to =~ m{^(?:\w+:|//)} ) { | |||
613 | 2 | 14 | return $c->reply->exception( | ||||
614 | q{`return_to` can not contain URL scheme or host}, | ||||||
615 | ); | ||||||
616 | } | ||||||
617 | |||||||
618 | 9 | 39 | $c->res->headers->location( $to ); | ||||
619 | 9 | 251 | return $c->rendered( 303 ); | ||||
620 | } | ||||||
621 | 3 | 31 | $c->flash( error => 'Username or password incorrect' ); | ||||
622 | 3 | 567 | 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 | 5 | my ( $self, $c ) = @_; | |||
632 | 1 | 50 | 8 | 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 | 18 | my ( $self, $c ) = @_; | |||
644 | 4 | 50 | 22 | 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 | 43 | my $schema_name = $self->schema; | ||||
651 | 4 | 54 | my $schema = $c->yancy->schema( $schema_name ); | ||||
652 | 4 | 0 | 26 | my $username_field = $self->username_field || $schema->{'x-id-field'} || 'id'; | |||
653 | 4 | 42 | my $password_field = $self->password_field; | ||||
654 | |||||||
655 | 4 | 38 | my $username = $c->param( $username_field ); | ||||
656 | 4 | 2387 | my $pass = $c->param( $self->password_field ); | ||||
657 | 4 | 100 | 295 | if ( $pass ne $c->param( $self->password_field . '-verify' ) ) { | |||
658 | 1 | 70 | 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 | 204 | 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 | 9 | map { $_ => $c->param( $_ ) } | ||||
680 | 6 | 104 | grep { !/^(?:$username_field|$password_field)$/ } | ||||
681 | 2 | 7 | @{ $self->register_fields } | ||||
2 | 11 | ||||||
682 | ), | ||||||
683 | }; | ||||||
684 | 2 | 150 | my $id = eval { $c->yancy->create( $schema_name, $item ) }; | ||||
2 | 9 | ||||||
685 | 2 | 100 | 50 | if ( my $exception = $@ ) { | |||
686 | 1 | 50 | 7 | my $error = ref $exception eq 'ARRAY' ? 'validation' : 'create'; | |||
687 | 1 | 9 | $c->app->log->error( 'Error creating user: ' . $exception ); | ||||
688 | 1 | 26 | 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 | 614 | return $c->redirect_to( 'yancy.auth.password.login' ); | ||||
700 | } | ||||||
701 | |||||||
702 | sub _get_digest { | ||||||
703 | 17 | 17 | 53 | my ( $type, @config ) = @_; | |||
704 | 17 | 34 | my $digest = eval { | ||||
705 | 17 | 149 | Digest->new( $type, @config ) | ||||
706 | }; | ||||||
707 | 17 | 50 | 934 | 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 | 64 | return $digest; | ||||
714 | } | ||||||
715 | |||||||
716 | sub _get_digest_by_config_string { | ||||||
717 | 17 | 17 | 53 | my ( $config_string ) = @_; | |||
718 | 17 | 60 | my @digest_parts = split /\$/, $config_string; | ||||
719 | 17 | 58 | return _get_digest( @digest_parts ); | ||||
720 | } | ||||||
721 | |||||||
722 | sub _build_digest_config_string { | ||||||
723 | 17 | 17 | 111 | my ( $config ) = @_; | |||
724 | my @config_parts = ( | ||||||
725 | 17 | 139 | map { $_, $config->{$_} } grep !/^type$/, keys %$config | ||||
0 | 0 | ||||||
726 | ); | ||||||
727 | 17 | 89 | return join '$', $config->{type}, @config_parts; | ||||
728 | } | ||||||
729 | |||||||
730 | sub _check_pass { | ||||||
731 | 14 | 14 | 52 | my ( $self, $c, $username, $input_password ) = @_; | |||
732 | 14 | 201 | my $user = $self->_get_user( $c, $username ); | ||||
733 | |||||||
734 | 14 | 100 | 59 | 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 | 26 | return undef; | ||||
740 | } | ||||||
741 | |||||||
742 | my ( $user_password, $user_digest_config_string ) | ||||||
743 | 13 | 77 | = split /\$/, $user->{ $self->password_field }, 2; | ||||
744 | |||||||
745 | 13 | 138 | my $force_upgrade = 0; | ||||
746 | 13 | 50 | 42 | 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 | 25 | my $digest = eval { _get_digest_by_config_string( $user_digest_config_string ) }; | ||||
13 | 54 | ||||||
758 | 13 | 50 | 49 | if ( $@ ) { | |||
759 | 0 | 0 | die sprintf 'Error checking password for user "%s": %s', $username, $@; | ||||
760 | } | ||||||
761 | 13 | 158 | my $check_password = $digest->add( $input_password )->b64digest; | ||||
762 | |||||||
763 | 13 | 45 | my $success = $check_password eq $user_password; | ||||
764 | |||||||
765 | 13 | 84 | my $default_config_string = _build_digest_config_string( $self->default_digest ); | ||||
766 | 13 | 100 | 66 | 117 | 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 | 9 | $self->_set_password( $c, $username, $input_password ); | ||||
770 | } | ||||||
771 | |||||||
772 | 13 | 130 | return $success; | ||||
773 | } | ||||||
774 | |||||||
775 | sub logout { | ||||||
776 | 6 | 6 | 0 | 19 | my ( $self, $c ) = @_; | ||
777 | 6 | 28 | delete $c->session->{yancy}{auth}{password}; | ||||
778 | } | ||||||
779 | |||||||
780 | sub _get_logout { | ||||||
781 | 3 | 3 | 10 | my ( $self, $c ) = @_; | |||
782 | 3 | 13 | $self->logout( $c ); | ||||
783 | 3 | 1525 | $c->res->code( 303 ); | ||||
784 | 3 | 100 | 50 | my $redirect_to = $c->param( 'redirect_to' ) // $c->req->headers->referrer // '/'; | |||
100 | |||||||
785 | 3 | 50 | 762 | if ( $redirect_to eq $c->req->url->path ) { | |||
786 | 0 | 0 | $redirect_to = '/'; | ||||
787 | } | ||||||
788 | 3 | 625 | return $c->redirect_to( $redirect_to ); | ||||
789 | } | ||||||
790 | |||||||
791 | 1; | ||||||
792 | |||||||
793 | __END__ |