blib/lib/Dancer/Plugin/Auth/Extensible.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 110 | 129 | 85.2 |
branch | 53 | 76 | 69.7 |
condition | 6 | 12 | 50.0 |
subroutine | 21 | 23 | 91.3 |
pod | 0 | 1 | 0.0 |
total | 190 | 241 | 78.8 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package Dancer::Plugin::Auth::Extensible; | ||||||
2 | |||||||
3 | 2 | 2 | 276695 | use warnings; | |||
2 | 2 | ||||||
2 | 55 | ||||||
4 | 2 | 2 | 6 | use strict; | |||
2 | 3 | ||||||
2 | 29 | ||||||
5 | |||||||
6 | 2 | 2 | 6 | use Carp; | |||
2 | 5 | ||||||
2 | 83 | ||||||
7 | 2 | 2 | 883 | use Dancer::Plugin; | |||
2 | 56330 | ||||||
2 | 128 | ||||||
8 | 2 | 2 | 604 | use Dancer qw(:syntax); | |||
2 | 107026 | ||||||
2 | 8 | ||||||
9 | |||||||
10 | our $VERSION = '1.00'; | ||||||
11 | |||||||
12 | my $settings = plugin_setting; | ||||||
13 | |||||||
14 | my $loginpage = $settings->{login_page} || '/login'; | ||||||
15 | my $userhomepage = $settings->{user_home_page} || '/'; | ||||||
16 | my $logoutpage = $settings->{logout_page} || '/logout'; | ||||||
17 | my $deniedpage = $settings->{denied_page} || '/login/denied'; | ||||||
18 | my $exitpage = $settings->{exit_page}; | ||||||
19 | |||||||
20 | |||||||
21 | =head1 NAME | ||||||
22 | |||||||
23 | Dancer::Plugin::Auth::Extensible - extensible authentication framework for Dancer apps | ||||||
24 | |||||||
25 | =head1 DESCRIPTION | ||||||
26 | |||||||
27 | A user authentication and authorisation framework plugin for Dancer apps. | ||||||
28 | |||||||
29 | Makes it easy to require a user to be logged in to access certain routes, | ||||||
30 | provides role-based access control, and supports various authentication | ||||||
31 | methods/sources (config file, database, Unix system users, etc). | ||||||
32 | |||||||
33 | Designed to support multiple authentication realms and to be as extensible as | ||||||
34 | possible, and to make secure password handling easy. The base class for auth | ||||||
35 | providers makes handling C |
||||||
36 | have no excuse for storing plain-text passwords. A simple script to generate | ||||||
37 | RFC2307-style hashed passwords is included, or you can use L |
||||||
38 | yourself to do so, or use the C |
||||||
39 | |||||||
40 | |||||||
41 | =head1 SYNOPSIS | ||||||
42 | |||||||
43 | Configure the plugin to use the authentication provider class you wish to use: | ||||||
44 | |||||||
45 | plugins: | ||||||
46 | Auth::Extensible: | ||||||
47 | realms: | ||||||
48 | users: | ||||||
49 | provider: Example | ||||||
50 | .... | ||||||
51 | |||||||
52 | The configuration you provide will depend on the authentication provider module | ||||||
53 | in use. For a simple example, see | ||||||
54 | L |
||||||
55 | |||||||
56 | Define that a user must be logged in and have the proper permissions to | ||||||
57 | access a route: | ||||||
58 | |||||||
59 | get '/secret' => require_role Confidant => sub { tell_secrets(); }; | ||||||
60 | |||||||
61 | Define that a user must be logged in to access a route - and find out who is | ||||||
62 | logged in with the C |
||||||
63 | |||||||
64 | get '/users' => require_login sub { | ||||||
65 | my $user = logged_in_user; | ||||||
66 | return "Hi there, $user->{username}"; | ||||||
67 | }; | ||||||
68 | |||||||
69 | =head1 AUTHENTICATION PROVIDERS | ||||||
70 | |||||||
71 | For flexibility, this authentication framework uses simple authentication | ||||||
72 | provider classes, which implement a simple interface and do whatever is required | ||||||
73 | to authenticate a user against the chosen source of authentication. | ||||||
74 | |||||||
75 | For an example of how simple provider classes are, so you can build your own if | ||||||
76 | required or just try out this authentication framework plugin easily, | ||||||
77 | see L |
||||||
78 | |||||||
79 | This framework supplies the following providers out-of-the-box: | ||||||
80 | |||||||
81 | =over 4 | ||||||
82 | |||||||
83 | =item L |
||||||
84 | |||||||
85 | Authenticates users using system accounts on Linux/Unix type boxes | ||||||
86 | |||||||
87 | =item L |
||||||
88 | |||||||
89 | Authenticates users stored in a database table | ||||||
90 | |||||||
91 | =item L |
||||||
92 | |||||||
93 | Authenticates users stored in the app's config | ||||||
94 | |||||||
95 | =back | ||||||
96 | |||||||
97 | Need to write your own? Just subclass | ||||||
98 | L |
||||||
99 | methods, and you're good to go! | ||||||
100 | |||||||
101 | =head1 CONTROLLING ACCESS TO ROUTES | ||||||
102 | |||||||
103 | Keywords are provided to check if a user is logged in / has appropriate roles. | ||||||
104 | |||||||
105 | =over | ||||||
106 | |||||||
107 | =item require_login - require the user to be logged in | ||||||
108 | |||||||
109 | get '/dashboard' => require_login sub { .... }; | ||||||
110 | |||||||
111 | If the user is not logged in, they will be redirected to the login page URL to | ||||||
112 | log in. The default URL is C - this may be changed with the | ||||||
113 | C |
||||||
114 | |||||||
115 | =item require_role - require the user to have a specified role | ||||||
116 | |||||||
117 | get '/beer' => require_role BeerDrinker => sub { ... }; | ||||||
118 | |||||||
119 | Requires that the user be logged in as a user who has the specified role. If | ||||||
120 | the user is not logged in, they will be redirected to the login page URL. If | ||||||
121 | they are logged in, but do not have the required role, they will be redirected | ||||||
122 | to the access denied URL. | ||||||
123 | |||||||
124 | =item require_any_roles - require the user to have one of a list of roles | ||||||
125 | |||||||
126 | get '/drink' => require_any_role [qw(BeerDrinker VodaDrinker)] => sub { | ||||||
127 | ... | ||||||
128 | }; | ||||||
129 | |||||||
130 | Requires that the user be logged in as a user who has any one (or more) of the | ||||||
131 | roles listed. If the user is not logged in, they will be redirected to the | ||||||
132 | login page URL. If they are logged in, but do not have any of the specified | ||||||
133 | roles, they will be redirected to the access denied URL. | ||||||
134 | |||||||
135 | =item require_all_roles - require the user to have all roles listed | ||||||
136 | |||||||
137 | get '/foo' => require_all_roles [qw(Foo Bar)] => sub { ... }; | ||||||
138 | |||||||
139 | Requires that the user be logged in as a user who has all of the roles listed. | ||||||
140 | If the user is not logged in, they will be redirected to the login page URL. If | ||||||
141 | they are logged in but do not have all of the specified roles, they will be | ||||||
142 | redirected to the access denied URL. | ||||||
143 | |||||||
144 | =back | ||||||
145 | |||||||
146 | =head2 Replacing the Default C< /login > and C< /login/denied > Routes | ||||||
147 | |||||||
148 | By default, the plugin adds a route to present a simple login form at that URL. | ||||||
149 | If you would rather add your own, set the C |
||||||
150 | value, and define your own route which responds to C with a login page. | ||||||
151 | Alternatively you can let DPAE add the routes and handle the status codes, etc. | ||||||
152 | and simply define the setting C |
||||||
153 | C |
||||||
154 | handle the route. Note that it must be a fully qualified sub. E.g. | ||||||
155 | |||||||
156 | plugins: | ||||||
157 | Auth::Extensible: | ||||||
158 | login_page_handler: 'My::App:login_page_handler' | ||||||
159 | permission_denied_page_handler: 'My::App:permission_denied_page_handler' | ||||||
160 | |||||||
161 | Then in your code you might simply use a template: | ||||||
162 | |||||||
163 | sub permission_denied_page_handler { | ||||||
164 | template 'account/login'; | ||||||
165 | } | ||||||
166 | |||||||
167 | |||||||
168 | If the user is logged in, but tries to access a route which requires a specific | ||||||
169 | role they don't have, they will be redirected to the "permission denied" page | ||||||
170 | URL, which defaults to C but may be changed using the | ||||||
171 | C |
||||||
172 | |||||||
173 | Again, by default a route is added to respond to that URL with a default page; | ||||||
174 | again, you can disable this by setting C |
||||||
175 | own. | ||||||
176 | |||||||
177 | This would still leave the routes C |
||||||
178 | routes in place. To disable them too, set the option C |
||||||
179 | to a true value. In this case, these routes should be defined by the user, | ||||||
180 | and should do at least the following: | ||||||
181 | |||||||
182 | post '/login' => sub { | ||||||
183 | my ($success, $realm) = authenticate_user( | ||||||
184 | params->{username}, params->{password} | ||||||
185 | ); | ||||||
186 | if ($success) { | ||||||
187 | session logged_in_user => params->{username}; | ||||||
188 | session logged_in_user_realm => $realm; | ||||||
189 | # other code here | ||||||
190 | } else { | ||||||
191 | # authentication failed | ||||||
192 | } | ||||||
193 | }; | ||||||
194 | |||||||
195 | any '/logout' => sub { | ||||||
196 | session->destroy; | ||||||
197 | }; | ||||||
198 | |||||||
199 | If you want to use the default C |
||||||
200 | you can configure them. See below. | ||||||
201 | |||||||
202 | =head2 Keywords | ||||||
203 | |||||||
204 | =over | ||||||
205 | |||||||
206 | =item require_login | ||||||
207 | |||||||
208 | Used to wrap a route which requires a user to be logged in order to access | ||||||
209 | it. | ||||||
210 | |||||||
211 | get '/secret' => require_login sub { .... }; | ||||||
212 | |||||||
213 | =cut | ||||||
214 | |||||||
215 | sub require_login { | ||||||
216 | 7 | 7 | 1084 | my $coderef = shift; | |||
217 | return sub { | ||||||
218 | 14 | 50 | 33 | 14 | 39915 | if (!$coderef || ref $coderef ne 'CODE') { | |
219 | 0 | 0 | croak "Invalid require_login usage, please see docs"; | ||||
220 | } | ||||||
221 | |||||||
222 | 14 | 26 | my $user = logged_in_user(); | ||||
223 | 14 | 100 | 32 | if (!$user) { | |||
224 | 3 | 10 | execute_hook('login_required', $coderef); | ||||
225 | # TODO: see if any code executed by that hook set up a response | ||||||
226 | 3 | 102 | return redirect uri_for($loginpage, { return_url => request->request_uri }); | ||||
227 | } | ||||||
228 | 11 | 33 | return $coderef->(); | ||||
229 | 7 | 27 | }; | ||||
230 | } | ||||||
231 | |||||||
232 | register require_login => \&require_login; | ||||||
233 | register requires_login => \&require_login; | ||||||
234 | |||||||
235 | =item require_role | ||||||
236 | |||||||
237 | Used to wrap a route which requires a user to be logged in as a user with the | ||||||
238 | specified role in order to access it. | ||||||
239 | |||||||
240 | get '/beer' => require_role BeerDrinker => sub { ... }; | ||||||
241 | |||||||
242 | You can also provide a regular expression, if you need to match the role using a | ||||||
243 | regex - for example: | ||||||
244 | |||||||
245 | get '/beer' => require_role qr/Drinker$/ => sub { ... }; | ||||||
246 | |||||||
247 | =cut | ||||||
248 | sub require_role { | ||||||
249 | 3 | 3 | 5 | return _build_wrapper(@_, 'single'); | |||
250 | } | ||||||
251 | |||||||
252 | register require_role => \&require_role; | ||||||
253 | register requires_role => \&require_role; | ||||||
254 | |||||||
255 | =item require_any_role | ||||||
256 | |||||||
257 | Used to wrap a route which requires a user to be logged in as a user with any | ||||||
258 | one (or more) of the specified roles in order to access it. | ||||||
259 | |||||||
260 | get '/foo' => require_any_role [qw(Foo Bar)] => sub { ... }; | ||||||
261 | |||||||
262 | =cut | ||||||
263 | |||||||
264 | sub require_any_role { | ||||||
265 | 1 | 1 | 2 | return _build_wrapper(@_, 'any'); | |||
266 | } | ||||||
267 | |||||||
268 | register require_any_role => \&require_any_role; | ||||||
269 | register requires_any_role => \&require_any_role; | ||||||
270 | |||||||
271 | =item require_all_roles | ||||||
272 | |||||||
273 | Used to wrap a route which requires a user to be logged in as a user with all | ||||||
274 | of the roles listed in order to access it. | ||||||
275 | |||||||
276 | get '/foo' => require_all_roles [qw(Foo Bar)] => sub { ... }; | ||||||
277 | |||||||
278 | =cut | ||||||
279 | |||||||
280 | sub require_all_roles { | ||||||
281 | 1 | 1 | 1 | return _build_wrapper(@_, 'all'); | |||
282 | } | ||||||
283 | |||||||
284 | register require_all_roles => \&require_all_roles; | ||||||
285 | register requires_all_roles => \&require_all_roles; | ||||||
286 | |||||||
287 | |||||||
288 | sub _build_wrapper { | ||||||
289 | 5 | 5 | 6 | my $require_role = shift; | |||
290 | 5 | 3 | my $coderef = shift; | ||||
291 | 5 | 5 | my $mode = shift; | ||||
292 | |||||||
293 | 5 | 100 | 10 | my @role_list = ref $require_role eq 'ARRAY' | |||
294 | ? @$require_role | ||||||
295 | : $require_role; | ||||||
296 | return sub { | ||||||
297 | 7 | 7 | 25542 | my $user = logged_in_user(); | |||
298 | 7 | 100 | 17 | if (!$user) { | |||
299 | 2 | 7 | execute_hook('login_required', $coderef); | ||||
300 | # TODO: see if any code executed by that hook set up a response | ||||||
301 | 2 | 67 | return redirect uri_for($loginpage, { return_url => request->request_uri }); | ||||
302 | } | ||||||
303 | |||||||
304 | 5 | 29 | my $role_match; | ||||
305 | 5 | 100 | 18 | if ($mode eq 'single') { | |||
100 | |||||||
50 | |||||||
306 | 3 | 5 | for (user_roles()) { | ||||
307 | 6 | 100 | 50 | 13 | $role_match++ and last if _smart_match($_, $require_role); | ||
308 | } | ||||||
309 | } elsif ($mode eq 'any') { | ||||||
310 | 1 | 2 | my %role_ok = map { $_ => 1 } @role_list; | ||||
2 | 5 | ||||||
311 | 1 | 2 | for (user_roles()) { | ||||
312 | 2 | 100 | 50 | 8 | $role_match++ and last if $role_ok{$_}; | ||
313 | } | ||||||
314 | } elsif ($mode eq 'all') { | ||||||
315 | 1 | 2 | $role_match++; | ||||
316 | 1 | 3 | for my $role (@role_list) { | ||||
317 | 2 | 50 | 5 | if (!user_has_role($role)) { | |||
318 | 0 | 0 | $role_match = 0; | ||||
319 | 0 | 0 | last; | ||||
320 | } | ||||||
321 | } | ||||||
322 | } | ||||||
323 | |||||||
324 | 5 | 100 | 10 | if ($role_match) { | |||
325 | # We're happy with their roles, so go head and execute the route | ||||||
326 | # handler coderef. | ||||||
327 | 4 | 13 | return $coderef->(); | ||||
328 | } | ||||||
329 | |||||||
330 | 1 | 5 | execute_hook('permission_denied', $coderef); | ||||
331 | # TODO: see if any code executed by that hook set up a response | ||||||
332 | 1 | 37 | return redirect uri_for($deniedpage, { return_url => request->request_uri }); | ||||
333 | 5 | 20 | }; | ||||
334 | } | ||||||
335 | |||||||
336 | |||||||
337 | =item logged_in_user | ||||||
338 | |||||||
339 | Returns a hashref of details of the currently logged-in user, if there is one. | ||||||
340 | |||||||
341 | The details you get back will depend upon the authentication provider in use. | ||||||
342 | |||||||
343 | =cut | ||||||
344 | |||||||
345 | sub logged_in_user { | ||||||
346 | 30 | 100 | 30 | 3580 | if (my $user = session 'logged_in_user') { | ||
347 | 20 | 2290 | my $realm = session 'logged_in_user_realm'; | ||||
348 | |||||||
349 | # First, if we've cached the details of this user earlier in this route | ||||||
350 | # execution in vars, just return it rather than ask the provider again | ||||||
351 | 20 | 100 | 1768 | if (my $cached = vars->{dpae_user_cache}{$realm}{$user}) { | |||
352 | 2 | 17 | return $cached; | ||||
353 | } | ||||||
354 | 18 | 123 | my $provider = auth_provider($realm); | ||||
355 | 18 | 40 | my $result = $provider->get_user_details($user, $realm); | ||||
356 | 18 | 30 | vars->{dpae_user_cache}{$realm}{$user} = $result; | ||||
357 | 18 | 91 | return $result; | ||||
358 | } else { | ||||||
359 | 10 | 1707 | return; | ||||
360 | } | ||||||
361 | } | ||||||
362 | register logged_in_user => \&logged_in_user; | ||||||
363 | |||||||
364 | =item user_has_role | ||||||
365 | |||||||
366 | Check if a user has the role named. | ||||||
367 | |||||||
368 | By default, the currently-logged-in user will be checked, so you need only name | ||||||
369 | the role you're looking for: | ||||||
370 | |||||||
371 | if (user_has_role('BeerDrinker')) { pour_beer(); } | ||||||
372 | |||||||
373 | You can also provide the username to check; | ||||||
374 | |||||||
375 | if (user_has_role($user, $role)) { .... } | ||||||
376 | |||||||
377 | =cut | ||||||
378 | |||||||
379 | sub user_has_role { | ||||||
380 | 2 | 2 | 1 | my ($username, $want_role); | |||
381 | 2 | 50 | 5 | if (@_ == 2) { | |||
382 | 0 | 0 | ($username, $want_role) = @_; | ||||
383 | } else { | ||||||
384 | 2 | 5 | $username = session 'logged_in_user'; | ||||
385 | 2 | 183 | $want_role = shift; | ||||
386 | } | ||||||
387 | |||||||
388 | 2 | 50 | 6 | return unless defined $username; | |||
389 | |||||||
390 | 2 | 4 | my $roles = user_roles($username); | ||||
391 | |||||||
392 | 2 | 5 | for my $has_role (@$roles) { | ||||
393 | 3 | 100 | 11 | return 1 if $has_role eq $want_role; | |||
394 | } | ||||||
395 | |||||||
396 | 0 | 0 | return 0; | ||||
397 | } | ||||||
398 | register user_has_role => \&user_has_role; | ||||||
399 | |||||||
400 | =item user_roles | ||||||
401 | |||||||
402 | Returns a list of the roles of a user. | ||||||
403 | |||||||
404 | By default, roles for the currently-logged-in user will be checked; | ||||||
405 | alternatively, you may supply a username to check. | ||||||
406 | |||||||
407 | Returns a list or arrayref depending on context. | ||||||
408 | |||||||
409 | =cut | ||||||
410 | |||||||
411 | sub user_roles { | ||||||
412 | 9 | 9 | 308 | my ($username, $realm) = @_; | |||
413 | 9 | 100 | 25 | $username = session 'logged_in_user' unless defined $username; | |||
414 | |||||||
415 | 9 | 100 | 442 | my $search_realm = ($realm ? $realm : ''); | |||
416 | |||||||
417 | # First, if we cached the roles they have earlier in the route execution, | ||||||
418 | # don't ask the provider again | ||||||
419 | 9 | 100 | 19 | if (my $cached = vars->{dpae_roles_cache}{$search_realm}{$username}) { | |||
420 | # Deref even if returning an arrayref, so calling code can't modify the | ||||||
421 | # cached entry | ||||||
422 | 1 | 50 | 8 | return wantarray ? @$cached : [ @$cached ]; | |||
423 | } | ||||||
424 | |||||||
425 | 8 | 41 | my $roles = auth_provider($search_realm)->get_user_roles($username); | ||||
426 | 8 | 50 | 17 | return unless defined $roles; | |||
427 | 8 | 13 | vars->{dpae_roles_cache}{$search_realm}{$username} = $roles; | ||||
428 | 8 | 100 | 63 | return wantarray ? @$roles : $roles; | |||
429 | } | ||||||
430 | register user_roles => \&user_roles; | ||||||
431 | |||||||
432 | |||||||
433 | =item authenticate_user | ||||||
434 | |||||||
435 | Usually you'll want to let the built-in login handling code deal with | ||||||
436 | authenticating users, but in case you need to do it yourself, this keyword | ||||||
437 | accepts a username and password, and optionally a specific realm, and checks | ||||||
438 | whether the username and password are valid. | ||||||
439 | |||||||
440 | For example: | ||||||
441 | |||||||
442 | if (authenticate_user($username, $password)) { | ||||||
443 | ... | ||||||
444 | } | ||||||
445 | |||||||
446 | If you are using multiple authentication realms, by default each realm will be | ||||||
447 | consulted in turn. If you only wish to check one of them (for instance, you're | ||||||
448 | authenticating an admin user, and there's only one realm which applies to them), | ||||||
449 | you can supply the realm as an optional third parameter. | ||||||
450 | |||||||
451 | In boolean context, returns simply true or false; in list context, returns | ||||||
452 | C<($success, $realm)>. | ||||||
453 | |||||||
454 | =cut | ||||||
455 | |||||||
456 | sub authenticate_user { | ||||||
457 | 5 | 5 | 8 | my ($username, $password, $realm) = @_; | |||
458 | |||||||
459 | 5 | 50 | 25 | my @realms_to_check = $realm? ($realm) : (keys %{ $settings->{realms} }); | |||
5 | 30 | ||||||
460 | |||||||
461 | 5 | 10 | for my $realm (@realms_to_check) { | ||||
462 | 8 | 38 | debug "Attempting to authenticate $username against realm $realm"; | ||||
463 | 8 | 336 | my $provider = auth_provider($realm); | ||||
464 | 8 | 100 | 30 | if ($provider->authenticate_user($username, $password)) { | |||
465 | 4 | 3440 | debug "$realm accepted user $username"; | ||||
466 | 4 | 50 | 156 | return wantarray ? (1, $realm) : 1; | |||
467 | } | ||||||
468 | } | ||||||
469 | |||||||
470 | # If we get to here, we failed to authenticate against any realm using the | ||||||
471 | # details provided. | ||||||
472 | # TODO: allow providers to raise an exception if something failed, and catch | ||||||
473 | # that and do something appropriate, rather than just treating it as a | ||||||
474 | # failed login. | ||||||
475 | 1 | 50 | 5 | return wantarray ? (0, undef) : 0; | |||
476 | } | ||||||
477 | |||||||
478 | register authenticate_user => \&authenticate_user; | ||||||
479 | |||||||
480 | |||||||
481 | =back | ||||||
482 | |||||||
483 | =head2 SAMPLE CONFIGURATION | ||||||
484 | |||||||
485 | In your application's configuation file: | ||||||
486 | |||||||
487 | session: simple | ||||||
488 | plugins: | ||||||
489 | Auth::Extensible: | ||||||
490 | # Set to 1 if you want to disable the use of roles (0 is default) | ||||||
491 | disable_roles: 0 | ||||||
492 | # After /login: If no return_url is given: land here ('/' is default) | ||||||
493 | user_home_page: '/user' | ||||||
494 | # After /logout: If no return_url is given: land here (no default) | ||||||
495 | exit_page: '/' | ||||||
496 | |||||||
497 | # List each authentication realm, with the provider to use and the | ||||||
498 | # provider-specific settings (see the documentation for the provider | ||||||
499 | # you wish to use) | ||||||
500 | realms: | ||||||
501 | realm_one: | ||||||
502 | provider: Database | ||||||
503 | db_connection_name: 'foo' | ||||||
504 | |||||||
505 | B |
||||||
506 | authentication framework requires sessions in order to track information about | ||||||
507 | the currently logged in user. | ||||||
508 | Please see L |
||||||
509 | management within your application. | ||||||
510 | |||||||
511 | =cut | ||||||
512 | |||||||
513 | # Given a realm, returns a configured and ready to use instance of the provider | ||||||
514 | # specified by that realm's config. | ||||||
515 | { | ||||||
516 | my %realm_provider; | ||||||
517 | sub auth_provider { | ||||||
518 | 34 | 34 | 0 | 33 | my $realm = shift; | ||
519 | |||||||
520 | # If no realm was provided, but we have a logged in user, use their realm: | ||||||
521 | 34 | 50 | 66 | 80 | if (!$realm && session->{logged_in_user}) { | ||
522 | 7 | 585 | $realm = session->{logged_in_user_realm}; | ||||
523 | } | ||||||
524 | |||||||
525 | # First, if we already have a provider for this realm, go ahead and use it: | ||||||
526 | 34 | 100 | 673 | return $realm_provider{$realm} if exists $realm_provider{$realm}; | |||
527 | |||||||
528 | # OK, we need to find out what provider this realm uses, and get an instance | ||||||
529 | # of that provider, configured with the settings from the realm. | ||||||
530 | 2 | 50 | 6 | my $realm_settings = $settings->{realms}{$realm} | |||
531 | or die "Invalid realm $realm"; | ||||||
532 | my $provider_class = $realm_settings->{provider} | ||||||
533 | 2 | 50 | 6 | or die "No provider configured - consult documentation for " | |||
534 | . __PACKAGE__; | ||||||
535 | |||||||
536 | 2 | 50 | 6 | if ($provider_class !~ /::/) { | |||
537 | 2 | 6 | $provider_class = __PACKAGE__ . "::Provider::$provider_class"; | ||||
538 | } | ||||||
539 | 2 | 11 | my ($ok, $error) = Dancer::ModuleLoader->load($provider_class); | ||||
540 | |||||||
541 | 2 | 50 | 44 | if (! $ok) { | |||
542 | 0 | 0 | die "Cannot load provider $provider_class: $error"; | ||||
543 | } | ||||||
544 | |||||||
545 | 2 | 13 | return $realm_provider{$realm} = $provider_class->new($realm_settings); | ||||
546 | } | ||||||
547 | } | ||||||
548 | |||||||
549 | register_hook qw(login_required permission_denied); | ||||||
550 | register_plugin for_versions => [qw(1 2)]; | ||||||
551 | |||||||
552 | |||||||
553 | # Given a class method name and a set of parameters, try calling that class | ||||||
554 | # method for each realm in turn, arranging for each to receive the configuration | ||||||
555 | # defined for that realm, until one returns a non-undef, then return the realm which | ||||||
556 | # succeeded and the response. | ||||||
557 | # Note: all provider class methods return a single value; if any need to return | ||||||
558 | # a list in future, this will need changing) | ||||||
559 | sub _try_realms { | ||||||
560 | 0 | 0 | 0 | my ($method, @args); | |||
561 | 0 | 0 | for my $realm (keys %{ $settings->{realms} }) { | ||||
0 | 0 | ||||||
562 | 0 | 0 | my $provider = auth_provider($realm); | ||||
563 | 0 | 0 | 0 | if (!$provider->can($method)) { | |||
564 | 0 | 0 | die "Provider $provider does not provide a $method method!"; | ||||
565 | } | ||||||
566 | 0 | 0 | 0 | if (defined(my $result = $provider->$method(@args))) { | |||
567 | 0 | 0 | return $result; | ||||
568 | } | ||||||
569 | } | ||||||
570 | 0 | 0 | return; | ||||
571 | } | ||||||
572 | |||||||
573 | # Set up routes to serve default pages, if desired | ||||||
574 | if ( !$settings->{no_default_pages} ) { | ||||||
575 | get $loginpage => sub { | ||||||
576 | if(logged_in_user()) { | ||||||
577 | redirect params->{return_url} || $userhomepage; | ||||||
578 | } | ||||||
579 | |||||||
580 | status 401; | ||||||
581 | my $_default_login_page = | ||||||
582 | $settings->{login_page_handler} || '_default_login_page'; | ||||||
583 | 2 | 2 | 2730 | no strict 'refs'; | |||
2 | 4 | ||||||
2 | 126 | ||||||
584 | return &{$_default_login_page}(); | ||||||
585 | }; | ||||||
586 | get $deniedpage => sub { | ||||||
587 | status 403; | ||||||
588 | my $_default_permission_denied_page = | ||||||
589 | $settings->{permission_denied_page_handler} | ||||||
590 | || '_default_permission_denied_page'; | ||||||
591 | 2 | 2 | 8 | no strict 'refs'; | |||
2 | 2 | ||||||
2 | 717 | ||||||
592 | return &{$_default_permission_denied_page}(); | ||||||
593 | }; | ||||||
594 | } | ||||||
595 | |||||||
596 | |||||||
597 | # If no_login_handler is set, let the user do the login/logout herself | ||||||
598 | if (!$settings->{no_login_handler}) { | ||||||
599 | |||||||
600 | # Handle logging in... | ||||||
601 | post $loginpage => sub { | ||||||
602 | # For security, ensure the username and password are straight scalars; if | ||||||
603 | # the app is using a serializer and we were sent a blob of JSON, they could | ||||||
604 | # have come from that JSON, and thus could be hashrefs (JSON SQL injection) | ||||||
605 | # - for database providers, feeding a carefully crafted hashref to the SQL | ||||||
606 | # builder could result in different SQL to what we'd expect. | ||||||
607 | # For instance, if we pass password => params->{password} to an SQL builder, | ||||||
608 | # we'd expect the query to include e.g. "WHERE password = '...'" (likely | ||||||
609 | # with paremeterisation) - but if params->{password} was something | ||||||
610 | # different, e.g. { 'like' => '%' }, we might end up with some SQL like | ||||||
611 | # WHERE password LIKE '%' instead - which would not be a Good Thing. | ||||||
612 | my ($username, $password) = @{ params() }{qw(username password)}; | ||||||
613 | for ($username, $password) { | ||||||
614 | if (ref $_) { | ||||||
615 | # TODO: handle more cleanly | ||||||
616 | die "Attempt to pass a reference as username/password blocked"; | ||||||
617 | } | ||||||
618 | } | ||||||
619 | |||||||
620 | if(logged_in_user()) { | ||||||
621 | redirect params->{return_url} || $userhomepage; | ||||||
622 | } | ||||||
623 | |||||||
624 | my ($success, $realm) = authenticate_user( | ||||||
625 | $username, $password | ||||||
626 | ); | ||||||
627 | if ($success) { | ||||||
628 | session logged_in_user => $username; | ||||||
629 | session logged_in_user_realm => $realm; | ||||||
630 | redirect params->{return_url} || $userhomepage; | ||||||
631 | } else { | ||||||
632 | vars->{login_failed}++; | ||||||
633 | forward $loginpage, { login_failed => 1 }, { method => 'GET' }; | ||||||
634 | } | ||||||
635 | }; | ||||||
636 | |||||||
637 | # ... and logging out. | ||||||
638 | any ['get','post'] => $logoutpage => sub { | ||||||
639 | session->destroy; | ||||||
640 | if (params->{return_url}) { | ||||||
641 | redirect params->{return_url}; | ||||||
642 | } elsif ($exitpage) { | ||||||
643 | redirect $exitpage; | ||||||
644 | } else { | ||||||
645 | # TODO: perhaps make this more configurable, perhaps by attempting to | ||||||
646 | # render a template first. | ||||||
647 | return "OK, logged out successfully."; | ||||||
648 | } | ||||||
649 | }; | ||||||
650 | |||||||
651 | } | ||||||
652 | |||||||
653 | |||||||
654 | sub _default_permission_denied_page { | ||||||
655 | return < | ||||||
656 | Permission Denied |
||||||
657 | |||||||
658 |
|
||||||
659 | Sorry, you're not allowed to access that page. | ||||||
660 | |||||||
661 | PAGE | ||||||
662 | 0 | 0 | 0 | } | |||
663 | |||||||
664 | sub _default_login_page { | ||||||
665 | my $login_fail_message = vars->{login_failed} | ||||||
666 | 1 | 50 | 1 | 5 | ? " LOGIN FAILED " |
||
667 | : ""; | ||||||
668 | 1 | 50 | 10 | my $return_url = params->{return_url} || ''; | |||
669 | 1 | 29 | return < | ||||
670 | Login Required |
||||||
671 | |||||||
672 |
|
||||||
673 | You need to log in to continue. | ||||||
674 | |||||||
675 | |||||||
676 | $login_fail_message | ||||||
677 | |||||||
678 | |||||||
679 | |||||||
680 | |||||||
681 | |
||||||
682 | |||||||
683 | |||||||
684 | |
||||||
685 | |||||||
686 | |||||||
687 | |||||||
688 | PAGE | ||||||
689 | } | ||||||
690 | |||||||
691 | # Replacement for much maligned and misunderstood smartmatch operator | ||||||
692 | sub _smart_match { | ||||||
693 | 6 | 6 | 6 | my ($got, $want) = @_; | |||
694 | 6 | 100 | 13 | if (!ref $want) { | |||
50 | |||||||
0 | |||||||
695 | 4 | 12 | return $got eq $want; | ||||
696 | } elsif (ref $want eq 'Regexp') { | ||||||
697 | 2 | 15 | return $got =~ $want; | ||||
698 | } elsif (ref $want eq 'ARRAY') { | ||||||
699 | 0 | return grep { $_ eq $got } @$want; | |||||
0 | |||||||
700 | } else { | ||||||
701 | 0 | carp "Don't know how to match against a " . ref $want; | |||||
702 | } | ||||||
703 | } | ||||||
704 | |||||||
705 | |||||||
706 | |||||||
707 | |||||||
708 | =head1 AUTHOR | ||||||
709 | |||||||
710 | David Precious, C<< |
||||||
711 | |||||||
712 | |||||||
713 | =head1 BUGS / FEATURE REQUESTS | ||||||
714 | |||||||
715 | This is an early version; there may still be bugs present or features missing. | ||||||
716 | |||||||
717 | This is developed on GitHub - please feel free to raise issues or pull requests | ||||||
718 | against the repo at: | ||||||
719 | L |
||||||
720 | |||||||
721 | |||||||
722 | |||||||
723 | =head1 ACKNOWLEDGEMENTS | ||||||
724 | |||||||
725 | Valuable feedback on the early design of this module came from many people, | ||||||
726 | including Matt S Trout (mst), David Golden (xdg), Damien Krotkine (dams), | ||||||
727 | Daniel Perrett, and others. | ||||||
728 | |||||||
729 | Configurable login/logout URLs added by Rene (hertell) | ||||||
730 | |||||||
731 | Regex support for require_role by chenryn | ||||||
732 | |||||||
733 | Support for user_roles looking in other realms by Colin Ewen (casao) | ||||||
734 | |||||||
735 | Config options for default login/logout handlers by Henk van Oers (hvoers) | ||||||
736 | |||||||
737 | =head1 LICENSE AND COPYRIGHT | ||||||
738 | |||||||
739 | |||||||
740 | Copyright 2012-16 David Precious. | ||||||
741 | |||||||
742 | This program is free software; you can redistribute it and/or modify it | ||||||
743 | under the terms of either: the GNU General Public License as published | ||||||
744 | by the Free Software Foundation; or the Artistic License. | ||||||
745 | |||||||
746 | See http://dev.perl.org/licenses/ for more information. | ||||||
747 | |||||||
748 | |||||||
749 | =cut | ||||||
750 | |||||||
751 | 1; # End of Dancer::Plugin::Auth::Extensible |