| blib/lib/Yancy/Plugin/Auth.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % |
| statement | 33 | 34 | 97.0 |
| branch | 3 | 4 | 75.0 |
| condition | 5 | 5 | 100.0 |
| subroutine | 10 | 10 | 100.0 |
| pod | 4 | 4 | 100.0 |
| total | 55 | 57 | 96.4 |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | package Yancy::Plugin::Auth; | ||||||
| 2 | our $VERSION = '1.087'; | ||||||
| 3 | # ABSTRACT: Add one or more authentication plugins to your site | ||||||
| 4 | |||||||
| 5 | #pod =head1 SYNOPSIS | ||||||
| 6 | #pod | ||||||
| 7 | #pod use Mojolicious::Lite; | ||||||
| 8 | #pod plugin Yancy => { | ||||||
| 9 | #pod backend => 'sqlite://myapp.db', | ||||||
| 10 | #pod schema => { | ||||||
| 11 | #pod users => { | ||||||
| 12 | #pod properties => { | ||||||
| 13 | #pod id => { type => 'integer', readOnly => 1 }, | ||||||
| 14 | #pod plugin => { | ||||||
| 15 | #pod type => 'string', | ||||||
| 16 | #pod enum => [qw( password token )], | ||||||
| 17 | #pod }, | ||||||
| 18 | #pod username => { type => 'string' }, | ||||||
| 19 | #pod # Optional password for Password auth | ||||||
| 20 | #pod password => { type => 'string' }, | ||||||
| 21 | #pod }, | ||||||
| 22 | #pod }, | ||||||
| 23 | #pod }, | ||||||
| 24 | #pod }; | ||||||
| 25 | #pod app->yancy->plugin( 'Auth' => { | ||||||
| 26 | #pod schema => 'users', | ||||||
| 27 | #pod username_field => 'username', | ||||||
| 28 | #pod password_field => 'password', | ||||||
| 29 | #pod plugin_field => 'plugin', | ||||||
| 30 | #pod plugins => [ | ||||||
| 31 | #pod [ | ||||||
| 32 | #pod Password => { | ||||||
| 33 | #pod password_digest => { | ||||||
| 34 | #pod type => 'SHA-1', | ||||||
| 35 | #pod }, | ||||||
| 36 | #pod }, | ||||||
| 37 | #pod ], | ||||||
| 38 | #pod 'Token', | ||||||
| 39 | #pod ], | ||||||
| 40 | #pod } ); | ||||||
| 41 | #pod | ||||||
| 42 | #pod =head1 DESCRIPTION | ||||||
| 43 | #pod | ||||||
| 44 | #pod B |
||||||
| 45 | #pod Yancy v2.000 is released. | ||||||
| 46 | #pod | ||||||
| 47 | #pod This plugin adds authentication to your site. | ||||||
| 48 | #pod | ||||||
| 49 | #pod Multiple authentication plugins can be added with this plugin. If you | ||||||
| 50 | #pod only ever want to have one type of auth, you can use that auth plugin | ||||||
| 51 | #pod directly if you want. | ||||||
| 52 | #pod | ||||||
| 53 | #pod This module composes the L |
||||||
| 54 | #pod to provide the | ||||||
| 55 | #pod L |
||||||
| 56 | #pod authorization method. | ||||||
| 57 | #pod | ||||||
| 58 | #pod =head1 CONFIGURATION | ||||||
| 59 | #pod | ||||||
| 60 | #pod This plugin has the following configuration options. | ||||||
| 61 | #pod | ||||||
| 62 | #pod =head2 schema | ||||||
| 63 | #pod | ||||||
| 64 | #pod The name of the Yancy schema that holds users. Required. | ||||||
| 65 | #pod | ||||||
| 66 | #pod =head2 username_field | ||||||
| 67 | #pod | ||||||
| 68 | #pod The name of the field in the schema which is the user's identifier. | ||||||
| 69 | #pod This can be a user name, ID, or e-mail address, and is provided by the | ||||||
| 70 | #pod user during login. | ||||||
| 71 | #pod | ||||||
| 72 | #pod =head2 password_field | ||||||
| 73 | #pod | ||||||
| 74 | #pod The name of the field to use for the password or secret. | ||||||
| 75 | #pod | ||||||
| 76 | #pod =head2 plugin_field | ||||||
| 77 | #pod | ||||||
| 78 | #pod The field to store which plugin the user is using to authenticate. This | ||||||
| 79 | #pod field is only used if two auth plugins have the same username field. | ||||||
| 80 | #pod | ||||||
| 81 | #pod =head2 plugins | ||||||
| 82 | #pod | ||||||
| 83 | #pod An array of auth plugins to configure. Each plugin can be either a name | ||||||
| 84 | #pod (in the C |
||||||
| 85 | #pod two elements: The name (in the C |
||||||
| 86 | #pod hash reference of configuration. | ||||||
| 87 | #pod | ||||||
| 88 | #pod Each of this module's configuration keys will be used as the default for | ||||||
| 89 | #pod all the other auth plugins. Other plugins can override this | ||||||
| 90 | #pod configuration individually. For example, users and tokens can be stored | ||||||
| 91 | #pod in different schemas: | ||||||
| 92 | #pod | ||||||
| 93 | #pod app->yancy->plugin( 'Auth' => { | ||||||
| 94 | #pod plugins => [ | ||||||
| 95 | #pod [ | ||||||
| 96 | #pod 'Password', | ||||||
| 97 | #pod { | ||||||
| 98 | #pod schema => 'users', | ||||||
| 99 | #pod username_field => 'username', | ||||||
| 100 | #pod password_field => 'password', | ||||||
| 101 | #pod password_digest => { type => 'SHA-1' }, | ||||||
| 102 | #pod }, | ||||||
| 103 | #pod ], | ||||||
| 104 | #pod [ | ||||||
| 105 | #pod 'Token', | ||||||
| 106 | #pod { | ||||||
| 107 | #pod schema => 'tokens', | ||||||
| 108 | #pod token_field => 'token', | ||||||
| 109 | #pod }, | ||||||
| 110 | #pod ], | ||||||
| 111 | #pod ], | ||||||
| 112 | #pod } ); | ||||||
| 113 | #pod | ||||||
| 114 | #pod =head2 Single User / Multiple Auth | ||||||
| 115 | #pod | ||||||
| 116 | #pod To allow a single user to configure multiple authentication mechanisms, do not | ||||||
| 117 | #pod configure a C |
||||||
| 118 | #pod C |
||||||
| 119 | #pod can log in and register with another auth method to link to the same account. | ||||||
| 120 | #pod | ||||||
| 121 | #pod =head2 Sessions | ||||||
| 122 | #pod | ||||||
| 123 | #pod This module uses L | ||||||
| 124 | #pod sessions|https://mojolicious.org/perldoc/Mojolicious/Controller#session> | ||||||
| 125 | #pod to store the login information in a secure, signed cookie. | ||||||
| 126 | #pod | ||||||
| 127 | #pod To configure the default expiration of a session, use | ||||||
| 128 | #pod L | ||||||
| 129 | #pod default_expiration|https://mojolicious.org/perldoc/Mojolicious/Sessions#default_expiration>. | ||||||
| 130 | #pod | ||||||
| 131 | #pod use Mojolicious::Lite; | ||||||
| 132 | #pod # Expire a session after 1 day of inactivity | ||||||
| 133 | #pod app->sessions->default_expiration( 24 * 60 * 60 ); | ||||||
| 134 | #pod | ||||||
| 135 | #pod =head1 HELPERS | ||||||
| 136 | #pod | ||||||
| 137 | #pod This plugin has the following helpers. | ||||||
| 138 | #pod | ||||||
| 139 | #pod =head2 yancy.auth.current_user | ||||||
| 140 | #pod | ||||||
| 141 | #pod Get the current user from one of the configured plugins, if any. Returns | ||||||
| 142 | #pod C |
||||||
| 143 | #pod | ||||||
| 144 | #pod my $user = $c->yancy->auth->current_user | ||||||
| 145 | #pod || return $c->render( status => 401, text => 'Unauthorized' ); | ||||||
| 146 | #pod | ||||||
| 147 | #pod =head2 yancy.auth.require_user | ||||||
| 148 | #pod | ||||||
| 149 | #pod Validate there is a logged-in user and optionally that the user data has | ||||||
| 150 | #pod certain values. See L |
||||||
| 151 | #pod | ||||||
| 152 | #pod # Display the user dashboard, but only to logged-in users | ||||||
| 153 | #pod my $auth_route = $app->routes->under( '/user', $app->yancy->auth->require_user ); | ||||||
| 154 | #pod $auth_route->get( '' )->to( 'user#dashboard' ); | ||||||
| 155 | #pod | ||||||
| 156 | #pod =head2 yancy.auth.login_form | ||||||
| 157 | #pod | ||||||
| 158 | #pod Return an HTML string containing the rendered login forms for all | ||||||
| 159 | #pod configured auth plugins, in order. | ||||||
| 160 | #pod | ||||||
| 161 | #pod %# Display a login form to an unauthenticated visitor | ||||||
| 162 | #pod % if ( !$c->yancy->auth->current_user ) { | ||||||
| 163 | #pod %= $c->yancy->auth->login_form | ||||||
| 164 | #pod % } | ||||||
| 165 | #pod | ||||||
| 166 | #pod =head2 yancy.auth.logout | ||||||
| 167 | #pod | ||||||
| 168 | #pod Log out any current account from any auth plugin. Use this in your own | ||||||
| 169 | #pod route handlers to perform a logout. | ||||||
| 170 | #pod | ||||||
| 171 | #pod =head1 ROUTES | ||||||
| 172 | #pod | ||||||
| 173 | #pod This plugin creates the following L | ||||||
| 174 | #pod routes|https://mojolicious.org/perldoc/Mojolicious/Guides/Routing#Named-routes>. | ||||||
| 175 | #pod Use named routes with helpers like | ||||||
| 176 | #pod L |
||||||
| 177 | #pod L |
||||||
| 178 | #pod L |
||||||
| 179 | #pod | ||||||
| 180 | #pod =head2 yancy.auth.login_form | ||||||
| 181 | #pod | ||||||
| 182 | #pod Display all of the login forms for the configured auth plugins. This route handles C |
||||||
| 183 | #pod requests and can be used with the L |
||||||
| 184 | #pod L |
||||||
| 185 | #pod and L |
||||||
| 186 | #pod | ||||||
| 187 | #pod %= link_to Login => 'yancy.auth.login_form' | ||||||
| 188 | #pod <%= link_to 'yancy.auth.login_form', begin %>Login<% end %> | ||||||
| 189 | #pod Login here: <%= url_for 'yancy.auth.login_form' %> |
||||||
| 190 | #pod | ||||||
| 191 | #pod =head2 yancy.auth.logout | ||||||
| 192 | #pod | ||||||
| 193 | #pod Log out of all configured auth plugins. This route handles C |
||||||
| 194 | #pod requests and can be used with the L |
||||||
| 195 | #pod L |
||||||
| 196 | #pod and L |
||||||
| 197 | #pod | ||||||
| 198 | #pod %= link_to Logout => 'yancy.auth.logout' | ||||||
| 199 | #pod <%= link_to 'yancy.auth.logout', begin %>Logout<% end %> | ||||||
| 200 | #pod Logout here: <%= url_for 'yancy.auth.logout' %> |
||||||
| 201 | #pod | ||||||
| 202 | #pod =head1 TEMPLATES | ||||||
| 203 | #pod | ||||||
| 204 | #pod To override these templates, add your own at the designated path inside | ||||||
| 205 | #pod your app's C |
||||||
| 206 | #pod | ||||||
| 207 | #pod =head2 yancy/auth/login_form.html.ep | ||||||
| 208 | #pod | ||||||
| 209 | #pod This displays all of the login forms for all of the configured plugins | ||||||
| 210 | #pod (if the plugin has a login form). | ||||||
| 211 | #pod | ||||||
| 212 | #pod =head2 yancy/auth/login_page.html.ep | ||||||
| 213 | #pod | ||||||
| 214 | #pod This displays the login form on a page directing the user to log in. | ||||||
| 215 | #pod | ||||||
| 216 | #pod =head2 layouts/yancy/auth.html.ep | ||||||
| 217 | #pod | ||||||
| 218 | #pod The layout that Yancy uses when displaying the login page, the | ||||||
| 219 | #pod unauthorized error message, and other auth-related pages. | ||||||
| 220 | #pod | ||||||
| 221 | #pod =head1 SEE ALSO | ||||||
| 222 | #pod | ||||||
| 223 | #pod =head2 Multiplex Plugins | ||||||
| 224 | #pod | ||||||
| 225 | #pod These are possible Auth plugins that can be used with this plugin (or as | ||||||
| 226 | #pod standalone, if desired). | ||||||
| 227 | #pod | ||||||
| 228 | #pod =over | ||||||
| 229 | #pod | ||||||
| 230 | #pod =item * L |
||||||
| 231 | #pod | ||||||
| 232 | #pod =item * L |
||||||
| 233 | #pod | ||||||
| 234 | #pod =item * L |
||||||
| 235 | #pod | ||||||
| 236 | #pod =item * L |
||||||
| 237 | #pod | ||||||
| 238 | #pod =back | ||||||
| 239 | #pod | ||||||
| 240 | #pod =cut | ||||||
| 241 | |||||||
| 242 | 2 | 2 | 3010 | use Mojo::Base 'Mojolicious::Plugin'; | |||
| 2 | 8 | ||||||
| 2 | 60 | ||||||
| 243 | 2 | 2 | 1523 | use Role::Tiny::With; | |||
| 2 | 650 | ||||||
| 2 | 157 | ||||||
| 244 | with 'Yancy::Plugin::Auth::Role::RequireUser'; | ||||||
| 245 | 2 | 2 | 16 | use Mojo::Loader qw( load_class ); | |||
| 2 | 3 | ||||||
| 2 | 98 | ||||||
| 246 | 2 | 2 | 13 | use Yancy::Util qw( currym match ); | |||
| 2 | 4 | ||||||
| 2 | 2399 | ||||||
| 247 | |||||||
| 248 | has _plugins => sub { [] }; | ||||||
| 249 | has route =>; | ||||||
| 250 | has logout_route =>; | ||||||
| 251 | |||||||
| 252 | sub register { | ||||||
| 253 | my ( $self, $app, $config ) = @_; | ||||||
| 254 | |||||||
| 255 | for my $plugin_conf ( @{ $config->{plugins} } ) { | ||||||
| 256 | my $name; | ||||||
| 257 | if ( !ref $plugin_conf ) { | ||||||
| 258 | $name = $plugin_conf; | ||||||
| 259 | $plugin_conf = {}; | ||||||
| 260 | } | ||||||
| 261 | else { | ||||||
| 262 | ( $name, $plugin_conf ) = @$plugin_conf; | ||||||
| 263 | } | ||||||
| 264 | |||||||
| 265 | # If we got a route config, we need to customize the plugin | ||||||
| 266 | # routes as well. If this plugin got its own "route" config, | ||||||
| 267 | # use it. Otherwise, build a route from the auth route and the | ||||||
| 268 | # plugin's moniker. | ||||||
| 269 | if ( my $route = $app->yancy->routify( $config->{route} ) ) { | ||||||
| 270 | $plugin_conf->{route} = $app->yancy->routify( | ||||||
| 271 | $plugin_conf->{route}, | ||||||
| 272 | $route->any( $plugin_conf->{moniker} || lc $name ), | ||||||
| 273 | ); | ||||||
| 274 | } | ||||||
| 275 | |||||||
| 276 | my %merged_conf = ( %$config, %$plugin_conf ); | ||||||
| 277 | if ( $plugin_conf->{username_field} ) { | ||||||
| 278 | # If this plugin has a unique username field, we don't need | ||||||
| 279 | # to specify a plugin field. This means a single user can | ||||||
| 280 | # have multiple auth mechanisms. | ||||||
| 281 | delete $merged_conf{ plugin_field }; | ||||||
| 282 | } | ||||||
| 283 | |||||||
| 284 | my $class = join '::', 'Yancy::Plugin::Auth', $name; | ||||||
| 285 | if ( my $e = load_class( $class ) ) { | ||||||
| 286 | die sprintf 'Unable to load auth plugin %s: %s', $name, $e; | ||||||
| 287 | } | ||||||
| 288 | my $plugin = $class->new( \%merged_conf ); | ||||||
| 289 | push @{ $self->_plugins }, $plugin; | ||||||
| 290 | # Plugin hashref overrides config from main Auth plugin | ||||||
| 291 | $plugin->init( $app, \%merged_conf ); | ||||||
| 292 | } | ||||||
| 293 | |||||||
| 294 | $app->helper( | ||||||
| 295 | 'yancy.auth.current_user' => currym( $self, 'current_user' ), | ||||||
| 296 | ); | ||||||
| 297 | $app->helper( | ||||||
| 298 | 'yancy.auth.plugins' => currym( $self, 'plugins' ), | ||||||
| 299 | ); | ||||||
| 300 | $app->helper( | ||||||
| 301 | 'yancy.auth.logout' => currym( $self, 'logout' ), | ||||||
| 302 | ); | ||||||
| 303 | $app->helper( | ||||||
| 304 | 'yancy.auth.login_form' => currym( $self, 'login_form' ), | ||||||
| 305 | ); | ||||||
| 306 | # Make this route after all the plugin routes so that it matches | ||||||
| 307 | # last. | ||||||
| 308 | $self->route( $app->yancy->routify( | ||||||
| 309 | $config->{route}, | ||||||
| 310 | $app->routes->get( '/yancy/auth' ), | ||||||
| 311 | ) ); | ||||||
| 312 | $self->logout_route( | ||||||
| 313 | $self->route->get( '/logout' )->to( cb => currym( $self, '_handle_logout' ) )->name( 'yancy.auth.logout' ) | ||||||
| 314 | ); | ||||||
| 315 | $self->route->get( '' )->to( cb => currym( $self, '_login_page' ) )->name( 'yancy.auth.login_form' ); | ||||||
| 316 | } | ||||||
| 317 | |||||||
| 318 | #pod =method current_user | ||||||
| 319 | #pod | ||||||
| 320 | #pod Returns the currently logged-in user, if any. | ||||||
| 321 | #pod | ||||||
| 322 | #pod =cut | ||||||
| 323 | |||||||
| 324 | sub current_user { | ||||||
| 325 | 22 | 22 | 1 | 50 | my ( $self, $c ) = @_; | ||
| 326 | 22 | 42 | for my $plugin ( @{ $self->_plugins } ) { | ||||
| 22 | 81 | ||||||
| 327 | 26 | 100 | 467 | if ( my $user = $plugin->current_user( $c ) ) { | |||
| 328 | 13 | 60 | return $user; | ||||
| 329 | } | ||||||
| 330 | } | ||||||
| 331 | 9 | 135 | return undef; | ||||
| 332 | } | ||||||
| 333 | |||||||
| 334 | #pod =method plugins | ||||||
| 335 | #pod | ||||||
| 336 | #pod Returns the list of configured auth plugins. | ||||||
| 337 | #pod | ||||||
| 338 | #pod =cut | ||||||
| 339 | |||||||
| 340 | sub plugins { | ||||||
| 341 | 3 | 3 | 1 | 7 | my ( $self, $c ) = @_; | ||
| 342 | 3 | 5 | return @{ $self->_plugins }; | ||||
| 3 | 11 | ||||||
| 343 | } | ||||||
| 344 | |||||||
| 345 | #pod =method login_form | ||||||
| 346 | #pod | ||||||
| 347 | #pod %= $c->yancy->auth->login_form | ||||||
| 348 | #pod | ||||||
| 349 | #pod Return the rendered login form template. | ||||||
| 350 | #pod | ||||||
| 351 | #pod =cut | ||||||
| 352 | |||||||
| 353 | sub login_form { | ||||||
| 354 | 5 | 5 | 1 | 16 | my ( $self, $c ) = @_; | ||
| 355 | 5 | 22 | return $c->render_to_string( | ||||
| 356 | template => 'yancy/auth/login_form', | ||||||
| 357 | plugins => $self->_plugins, | ||||||
| 358 | ); | ||||||
| 359 | } | ||||||
| 360 | |||||||
| 361 | sub _login_page { | ||||||
| 362 | 1 | 1 | 4 | my ( $self, $c ) = @_; | |||
| 363 | 1 | 7 | $c->render( | ||||
| 364 | template => 'yancy/auth/login_page', | ||||||
| 365 | plugins => $self->_plugins, | ||||||
| 366 | ); | ||||||
| 367 | } | ||||||
| 368 | |||||||
| 369 | #pod =method logout | ||||||
| 370 | #pod | ||||||
| 371 | #pod Log out the current user. Will call the C |
||||||
| 372 | #pod | ||||||
| 373 | #pod =cut | ||||||
| 374 | |||||||
| 375 | sub logout { | ||||||
| 376 | 3 | 3 | 1 | 7 | my ( $self, $c ) = @_; | ||
| 377 | 3 | 12 | $_->logout( $c ) for $self->plugins; | ||||
| 378 | } | ||||||
| 379 | |||||||
| 380 | sub _handle_logout { | ||||||
| 381 | 3 | 3 | 9 | my ( $self, $c ) = @_; | |||
| 382 | 3 | 12 | $self->logout( $c ); | ||||
| 383 | 3 | 11 | $c->res->code( 303 ); | ||||
| 384 | 3 | 100 | 52 | my $redirect_to = $c->param( 'redirect_to' ) // $c->req->headers->referrer // '/'; | |||
| 100 | |||||||
| 385 | 3 | 50 | 756 | if ( $redirect_to eq $c->req->url->path ) { | |||
| 386 | 0 | 0 | $redirect_to = '/'; | ||||
| 387 | } | ||||||
| 388 | 3 | 543 | return $c->redirect_to( $redirect_to ); | ||||
| 389 | } | ||||||
| 390 | |||||||
| 391 | 1; | ||||||
| 392 | |||||||
| 393 | __END__ |