| 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__ |