File Coverage

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.088';
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 This module is C and its API may change before
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 role
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 namespace) or an array reference with
85             #pod two elements: The name (in the C namespace) and a
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. Instead, give every authentication plugin its own
118             #pod C. Then, once a user has registered with one auth method, they
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 if no user was found in the session.
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, and
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 helpers.
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 helpers.
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 directory.
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   2050 use Mojo::Base 'Mojolicious::Plugin';
  2         5  
  2         38  
243 2     2   1385 use Role::Tiny::With;
  2         637  
  2         142  
244             with 'Yancy::Plugin::Auth::Role::RequireUser';
245 2     2   16 use Mojo::Loader qw( load_class );
  2         7  
  2         95  
246 2     2   12 use Yancy::Util qw( currym match );
  2         4  
  2         2354  
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 57 my ( $self, $c ) = @_;
326 22         38 for my $plugin ( @{ $self->_plugins } ) {
  22         70  
327 26 100       464 if ( my $user = $plugin->current_user( $c ) ) {
328 13         67 return $user;
329             }
330             }
331 9         154 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 9 my ( $self, $c ) = @_;
342 3         8 return @{ $self->_plugins };
  3         14  
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 18 my ( $self, $c ) = @_;
355 5         19 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   5 my ( $self, $c ) = @_;
363 1         6 $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 method on all configured auth plugins.
372             #pod
373             #pod =cut
374              
375             sub logout {
376 3     3 1 9 my ( $self, $c ) = @_;
377 3         28 $_->logout( $c ) for $self->plugins;
378             }
379              
380             sub _handle_logout {
381 3     3   12 my ( $self, $c ) = @_;
382 3         17 $self->logout( $c );
383 3         13 $c->res->code( 303 );
384 3   100     60 my $redirect_to = $c->param( 'redirect_to' ) // $c->req->headers->referrer // '/';
      100        
385 3 50       969 if ( $redirect_to eq $c->req->url->path ) {
386 0         0 $redirect_to = '/';
387             }
388 3         724 return $c->redirect_to( $redirect_to );
389             }
390              
391             1;
392              
393             __END__