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