blib/lib/Dancer/Plugin/Auth/Extensible.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 103 | 122 | 84.4 |
branch | 49 | 70 | 70.0 |
condition | 6 | 12 | 50.0 |
subroutine | 21 | 23 | 91.3 |
pod | 0 | 1 | 0.0 |
total | 179 | 228 | 78.5 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package Dancer::Plugin::Auth::Extensible; | ||||||
2 | |||||||
3 | 2 | 2 | 239574 | use warnings; | |||
2 | 4 | ||||||
2 | 64 | ||||||
4 | 2 | 2 | 7 | use strict; | |||
2 | 3 | ||||||
2 | 47 | ||||||
5 | |||||||
6 | 2 | 2 | 5 | use Carp; | |||
2 | 7 | ||||||
2 | 101 | ||||||
7 | 2 | 2 | 784 | use Dancer::Plugin; | |||
2 | 60296 | ||||||
2 | 130 | ||||||
8 | 2 | 2 | 473 | use Dancer qw(:syntax); | |||
2 | 139852 | ||||||
2 | 12 | ||||||
9 | |||||||
10 | our $VERSION = '0.40'; | ||||||
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 | 1050 | my $coderef = shift; | |||
217 | return sub { | ||||||
218 | 14 | 50 | 33 | 14 | 30775 | if (!$coderef || ref $coderef ne 'CODE') { | |
219 | 0 | 0 | croak "Invalid require_login usage, please see docs"; | ||||
220 | } | ||||||
221 | |||||||
222 | 14 | 27 | my $user = logged_in_user(); | ||||
223 | 14 | 100 | 28 | 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 | 120 | return redirect uri_for($loginpage, { return_url => request->request_uri }); | ||||
227 | } | ||||||
228 | 11 | 26 | return $coderef->(); | ||||
229 | 7 | 26 | }; | ||||
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 | 7 | 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 | 3 | 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 | 2 | 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 | 5 | my $require_role = shift; | |||
290 | 5 | 3 | my $coderef = shift; | ||||
291 | 5 | 5 | my $mode = shift; | ||||
292 | |||||||
293 | 5 | 100 | 13 | my @role_list = ref $require_role eq 'ARRAY' | |||
294 | ? @$require_role | ||||||
295 | : $require_role; | ||||||
296 | return sub { | ||||||
297 | 7 | 7 | 23686 | my $user = logged_in_user(); | |||
298 | 7 | 100 | 17 | if (!$user) { | |||
299 | 2 | 9 | execute_hook('login_required', $coderef); | ||||
300 | # TODO: see if any code executed by that hook set up a response | ||||||
301 | 2 | 76 | return redirect uri_for($loginpage, { return_url => request->request_uri }); | ||||
302 | } | ||||||
303 | |||||||
304 | 5 | 5 | my $role_match; | ||||
305 | 5 | 100 | 13 | if ($mode eq 'single') { | |||
100 | |||||||
50 | |||||||
306 | 3 | 6 | for (user_roles()) { | ||||
307 | 6 | 100 | 50 | 9 | $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 | 3 | for (user_roles()) { | ||||
312 | 2 | 100 | 50 | 7 | $role_match++ and last if $role_ok{$_}; | ||
313 | } | ||||||
314 | } elsif ($mode eq 'all') { | ||||||
315 | 1 | 1 | $role_match++; | ||||
316 | 1 | 2 | for my $role (@role_list) { | ||||
317 | 2 | 50 | 3 | if (!user_has_role($role)) { | |||
318 | 0 | 0 | $role_match = 0; | ||||
319 | 0 | 0 | last; | ||||
320 | } | ||||||
321 | } | ||||||
322 | } | ||||||
323 | |||||||
324 | 5 | 100 | 9 | if ($role_match) { | |||
325 | # We're happy with their roles, so go head and execute the route | ||||||
326 | # handler coderef. | ||||||
327 | 4 | 32 | return $coderef->(); | ||||
328 | } | ||||||
329 | |||||||
330 | 1 | 4 | execute_hook('permission_denied', $coderef); | ||||
331 | # TODO: see if any code executed by that hook set up a response | ||||||
332 | 1 | 38 | return redirect uri_for($deniedpage, { return_url => request->request_uri }); | ||||
333 | 5 | 27 | }; | ||||
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 | 28 | 100 | 28 | 63 | if (my $user = session 'logged_in_user') { | ||
347 | 18 | 1810 | my $realm = session 'logged_in_user_realm'; | ||||
348 | 18 | 1502 | my $provider = auth_provider($realm); | ||||
349 | 18 | 41 | return $provider->get_user_details($user, $realm); | ||||
350 | } else { | ||||||
351 | 10 | 1924 | return; | ||||
352 | } | ||||||
353 | } | ||||||
354 | register logged_in_user => \&logged_in_user; | ||||||
355 | |||||||
356 | =item user_has_role | ||||||
357 | |||||||
358 | Check if a user has the role named. | ||||||
359 | |||||||
360 | By default, the currently-logged-in user will be checked, so you need only name | ||||||
361 | the role you're looking for: | ||||||
362 | |||||||
363 | if (user_has_role('BeerDrinker')) { pour_beer(); } | ||||||
364 | |||||||
365 | You can also provide the username to check; | ||||||
366 | |||||||
367 | if (user_has_role($user, $role)) { .... } | ||||||
368 | |||||||
369 | =cut | ||||||
370 | |||||||
371 | sub user_has_role { | ||||||
372 | 2 | 2 | 3 | my ($username, $want_role); | |||
373 | 2 | 50 | 3 | if (@_ == 2) { | |||
374 | 0 | 0 | ($username, $want_role) = @_; | ||||
375 | } else { | ||||||
376 | 2 | 3 | $username = session 'logged_in_user'; | ||||
377 | 2 | 174 | $want_role = shift; | ||||
378 | } | ||||||
379 | |||||||
380 | 2 | 50 | 5 | return unless defined $username; | |||
381 | |||||||
382 | 2 | 3 | my $roles = user_roles($username); | ||||
383 | |||||||
384 | 2 | 3 | for my $has_role (@$roles) { | ||||
385 | 3 | 100 | 10 | return 1 if $has_role eq $want_role; | |||
386 | } | ||||||
387 | |||||||
388 | 0 | 0 | return 0; | ||||
389 | } | ||||||
390 | register user_has_role => \&user_has_role; | ||||||
391 | |||||||
392 | =item user_roles | ||||||
393 | |||||||
394 | Returns a list of the roles of a user. | ||||||
395 | |||||||
396 | By default, roles for the currently-logged-in user will be checked; | ||||||
397 | alternatively, you may supply a username to check. | ||||||
398 | |||||||
399 | Returns a list or arrayref depending on context. | ||||||
400 | |||||||
401 | =cut | ||||||
402 | |||||||
403 | sub user_roles { | ||||||
404 | 9 | 9 | 278 | my ($username, $realm) = @_; | |||
405 | 9 | 100 | 19 | $username = session 'logged_in_user' unless defined $username; | |||
406 | |||||||
407 | 9 | 100 | 439 | my $search_realm = ($realm ? $realm : ''); | |||
408 | |||||||
409 | 9 | 11 | my $roles = auth_provider($search_realm)->get_user_roles($username); | ||||
410 | 9 | 50 | 15 | return unless defined $roles; | |||
411 | 9 | 100 | 33 | return wantarray ? @$roles : $roles; | |||
412 | } | ||||||
413 | register user_roles => \&user_roles; | ||||||
414 | |||||||
415 | |||||||
416 | =item authenticate_user | ||||||
417 | |||||||
418 | Usually you'll want to let the built-in login handling code deal with | ||||||
419 | authenticating users, but in case you need to do it yourself, this keyword | ||||||
420 | accepts a username and password, and optionally a specific realm, and checks | ||||||
421 | whether the username and password are valid. | ||||||
422 | |||||||
423 | For example: | ||||||
424 | |||||||
425 | if (authenticate_user($username, $password)) { | ||||||
426 | ... | ||||||
427 | } | ||||||
428 | |||||||
429 | If you are using multiple authentication realms, by default each realm will be | ||||||
430 | consulted in turn. If you only wish to check one of them (for instance, you're | ||||||
431 | authenticating an admin user, and there's only one realm which applies to them), | ||||||
432 | you can supply the realm as an optional third parameter. | ||||||
433 | |||||||
434 | In boolean context, returns simply true or false; in list context, returns | ||||||
435 | C<($success, $realm)>. | ||||||
436 | |||||||
437 | =cut | ||||||
438 | |||||||
439 | sub authenticate_user { | ||||||
440 | 5 | 5 | 9 | my ($username, $password, $realm) = @_; | |||
441 | |||||||
442 | 5 | 50 | 14 | my @realms_to_check = $realm? ($realm) : (keys %{ $settings->{realms} }); | |||
5 | 22 | ||||||
443 | |||||||
444 | 5 | 9 | for my $realm (@realms_to_check) { | ||||
445 | 8 | 34 | debug "Attempting to authenticate $username against realm $realm"; | ||||
446 | 8 | 339 | my $provider = auth_provider($realm); | ||||
447 | 8 | 100 | 23 | if ($provider->authenticate_user($username, $password)) { | |||
448 | 4 | 3301 | debug "$realm accepted user $username"; | ||||
449 | 4 | 50 | 131 | return wantarray ? (1, $realm) : 1; | |||
450 | } | ||||||
451 | } | ||||||
452 | |||||||
453 | # If we get to here, we failed to authenticate against any realm using the | ||||||
454 | # details provided. | ||||||
455 | # TODO: allow providers to raise an exception if something failed, and catch | ||||||
456 | # that and do something appropriate, rather than just treating it as a | ||||||
457 | # failed login. | ||||||
458 | 1 | 50 | 6 | return wantarray ? (0, undef) : 0; | |||
459 | } | ||||||
460 | |||||||
461 | register authenticate_user => \&authenticate_user; | ||||||
462 | |||||||
463 | |||||||
464 | =back | ||||||
465 | |||||||
466 | =head2 SAMPLE CONFIGURATION | ||||||
467 | |||||||
468 | In your application's configuation file: | ||||||
469 | |||||||
470 | session: simple | ||||||
471 | plugins: | ||||||
472 | Auth::Extensible: | ||||||
473 | # Set to 1 if you want to disable the use of roles (0 is default) | ||||||
474 | disable_roles: 0 | ||||||
475 | # After /login: If no return_url is given: land here ('/' is default) | ||||||
476 | user_home_page: '/user' | ||||||
477 | # After /logout: If no return_url is given: land here (no default) | ||||||
478 | exit_page: '/' | ||||||
479 | |||||||
480 | # List each authentication realm, with the provider to use and the | ||||||
481 | # provider-specific settings (see the documentation for the provider | ||||||
482 | # you wish to use) | ||||||
483 | realms: | ||||||
484 | realm_one: | ||||||
485 | provider: Database | ||||||
486 | db_connection_name: 'foo' | ||||||
487 | |||||||
488 | B |
||||||
489 | authentication framework requires sessions in order to track information about | ||||||
490 | the currently logged in user. | ||||||
491 | Please see L |
||||||
492 | management within your application. | ||||||
493 | |||||||
494 | =cut | ||||||
495 | |||||||
496 | # Given a realm, returns a configured and ready to use instance of the provider | ||||||
497 | # specified by that realm's config. | ||||||
498 | { | ||||||
499 | my %realm_provider; | ||||||
500 | sub auth_provider { | ||||||
501 | 35 | 35 | 0 | 39 | my $realm = shift; | ||
502 | |||||||
503 | # If no realm was provided, but we have a logged in user, use their realm: | ||||||
504 | 35 | 100 | 66 | 81 | if (!$realm && session->{logged_in_user}) { | ||
505 | 8 | 711 | $realm = session->{logged_in_user_realm}; | ||||
506 | } | ||||||
507 | |||||||
508 | # First, if we already have a provider for this realm, go ahead and use it: | ||||||
509 | 35 | 100 | 793 | return $realm_provider{$realm} if exists $realm_provider{$realm}; | |||
510 | |||||||
511 | # OK, we need to find out what provider this realm uses, and get an instance | ||||||
512 | # of that provider, configured with the settings from the realm. | ||||||
513 | 2 | 50 | 11 | my $realm_settings = $settings->{realms}{$realm} | |||
514 | or die "Invalid realm $realm"; | ||||||
515 | 2 | 50 | 8 | my $provider_class = $realm_settings->{provider} | |||
516 | or die "No provider configured - consult documentation for " | ||||||
517 | . __PACKAGE__; | ||||||
518 | |||||||
519 | 2 | 50 | 10 | if ($provider_class !~ /::/) { | |||
520 | 2 | 8 | $provider_class = __PACKAGE__ . "::Provider::$provider_class"; | ||||
521 | } | ||||||
522 | 2 | 15 | my ($ok, $error) = Dancer::ModuleLoader->load($provider_class); | ||||
523 | |||||||
524 | 2 | 50 | 68 | if (! $ok) { | |||
525 | 0 | 0 | die "Cannot load provider $provider_class: $error"; | ||||
526 | } | ||||||
527 | |||||||
528 | 2 | 19 | return $realm_provider{$realm} = $provider_class->new($realm_settings); | ||||
529 | } | ||||||
530 | } | ||||||
531 | |||||||
532 | register_hook qw(login_required permission_denied); | ||||||
533 | register_plugin for_versions => [qw(1 2)]; | ||||||
534 | |||||||
535 | |||||||
536 | # Given a class method name and a set of parameters, try calling that class | ||||||
537 | # method for each realm in turn, arranging for each to receive the configuration | ||||||
538 | # defined for that realm, until one returns a non-undef, then return the realm which | ||||||
539 | # succeeded and the response. | ||||||
540 | # Note: all provider class methods return a single value; if any need to return | ||||||
541 | # a list in future, this will need changing) | ||||||
542 | sub _try_realms { | ||||||
543 | 0 | 0 | 0 | my ($method, @args); | |||
544 | 0 | 0 | for my $realm (keys %{ $settings->{realms} }) { | ||||
0 | 0 | ||||||
545 | 0 | 0 | my $provider = auth_provider($realm); | ||||
546 | 0 | 0 | 0 | if (!$provider->can($method)) { | |||
547 | 0 | 0 | die "Provider $provider does not provide a $method method!"; | ||||
548 | } | ||||||
549 | 0 | 0 | 0 | if (defined(my $result = $provider->$method(@args))) { | |||
550 | 0 | 0 | return $result; | ||||
551 | } | ||||||
552 | } | ||||||
553 | 0 | 0 | return; | ||||
554 | } | ||||||
555 | |||||||
556 | # Set up routes to serve default pages, if desired | ||||||
557 | if ( !$settings->{no_default_pages} ) { | ||||||
558 | get $loginpage => sub { | ||||||
559 | if(logged_in_user()) { | ||||||
560 | redirect params->{return_url} || $userhomepage; | ||||||
561 | } | ||||||
562 | |||||||
563 | status 401; | ||||||
564 | my $_default_login_page = | ||||||
565 | $settings->{login_page_handler} || '_default_login_page'; | ||||||
566 | 2 | 2 | 3606 | no strict 'refs'; | |||
2 | 5 | ||||||
2 | 237 | ||||||
567 | return &{$_default_login_page}(); | ||||||
568 | }; | ||||||
569 | get $deniedpage => sub { | ||||||
570 | status 403; | ||||||
571 | my $_default_permission_denied_page = | ||||||
572 | $settings->{permission_denied_page_handler} | ||||||
573 | || '_default_permission_denied_page'; | ||||||
574 | 2 | 2 | 12 | no strict 'refs'; | |||
2 | 3 | ||||||
2 | 1019 | ||||||
575 | return &{$_default_permission_denied_page}(); | ||||||
576 | }; | ||||||
577 | } | ||||||
578 | |||||||
579 | |||||||
580 | # If no_login_handler is set, let the user do the login/logout herself | ||||||
581 | if (!$settings->{no_login_handler}) { | ||||||
582 | |||||||
583 | # Handle logging in... | ||||||
584 | post $loginpage => sub { | ||||||
585 | # For security, ensure the username and password are straight scalars; if | ||||||
586 | # the app is using a serializer and we were sent a blob of JSON, they could | ||||||
587 | # have come from that JSON, and thus could be hashrefs (JSON SQL injection) | ||||||
588 | # - for database providers, feeding a carefully crafted hashref to the SQL | ||||||
589 | # builder could result in different SQL to what we'd expect. | ||||||
590 | # For instance, if we pass password => params->{password} to an SQL builder, | ||||||
591 | # we'd expect the query to include e.g. "WHERE password = '...'" (likely | ||||||
592 | # with paremeterisation) - but if params->{password} was something | ||||||
593 | # different, e.g. { 'like' => '%' }, we might end up with some SQL like | ||||||
594 | # WHERE password LIKE '%' instead - which would not be a Good Thing. | ||||||
595 | my ($username, $password) = @{ params() }{qw(username password)}; | ||||||
596 | for ($username, $password) { | ||||||
597 | if (ref $_) { | ||||||
598 | # TODO: handle more cleanly | ||||||
599 | die "Attempt to pass a reference as username/password blocked"; | ||||||
600 | } | ||||||
601 | } | ||||||
602 | |||||||
603 | if(logged_in_user()) { | ||||||
604 | redirect params->{return_url} || $userhomepage; | ||||||
605 | } | ||||||
606 | |||||||
607 | my ($success, $realm) = authenticate_user( | ||||||
608 | $username, $password | ||||||
609 | ); | ||||||
610 | if ($success) { | ||||||
611 | session logged_in_user => $username; | ||||||
612 | session logged_in_user_realm => $realm; | ||||||
613 | redirect params->{return_url} || $userhomepage; | ||||||
614 | } else { | ||||||
615 | vars->{login_failed}++; | ||||||
616 | forward $loginpage, { login_failed => 1 }, { method => 'GET' }; | ||||||
617 | } | ||||||
618 | }; | ||||||
619 | |||||||
620 | # ... and logging out. | ||||||
621 | any ['get','post'] => $logoutpage => sub { | ||||||
622 | session->destroy; | ||||||
623 | if (params->{return_url}) { | ||||||
624 | redirect params->{return_url}; | ||||||
625 | } elsif ($exitpage) { | ||||||
626 | redirect $exitpage; | ||||||
627 | } else { | ||||||
628 | # TODO: perhaps make this more configurable, perhaps by attempting to | ||||||
629 | # render a template first. | ||||||
630 | return "OK, logged out successfully."; | ||||||
631 | } | ||||||
632 | }; | ||||||
633 | |||||||
634 | } | ||||||
635 | |||||||
636 | |||||||
637 | sub _default_permission_denied_page { | ||||||
638 | return < | ||||||
639 | Permission Denied |
||||||
640 | |||||||
641 |
|
||||||
642 | Sorry, you're not allowed to access that page. | ||||||
643 | |||||||
644 | PAGE | ||||||
645 | 0 | 0 | 0 | } | |||
646 | |||||||
647 | sub _default_login_page { | ||||||
648 | 1 | 50 | 1 | 2 | my $login_fail_message = vars->{login_failed} | ||
649 | ? " LOGIN FAILED " |
||||||
650 | : ""; | ||||||
651 | 1 | 50 | 7 | my $return_url = params->{return_url} || ''; | |||
652 | 1 | 18 | return < | ||||
653 | Login Required |
||||||
654 | |||||||
655 |
|
||||||
656 | You need to log in to continue. | ||||||
657 | |||||||
658 | |||||||
659 | $login_fail_message | ||||||
660 | |||||||
661 | |||||||
662 | |||||||
663 | |||||||
664 | |
||||||
665 | |||||||
666 | |||||||
667 | |
||||||
668 | |||||||
669 | |||||||
670 | |||||||
671 | PAGE | ||||||
672 | } | ||||||
673 | |||||||
674 | # Replacement for much maligned and misunderstood smartmatch operator | ||||||
675 | sub _smart_match { | ||||||
676 | 6 | 6 | 7 | my ($got, $want) = @_; | |||
677 | 6 | 100 | 12 | if (!ref $want) { | |||
50 | |||||||
0 | |||||||
678 | 4 | 11 | return $got eq $want; | ||||
679 | } elsif (ref $want eq 'Regexp') { | ||||||
680 | 2 | 20 | return $got =~ $want; | ||||
681 | } elsif (ref $want eq 'ARRAY') { | ||||||
682 | 0 | return grep { $_ eq $got } @$want; | |||||
0 | |||||||
683 | } else { | ||||||
684 | 0 | carp "Don't know how to match against a " . ref $want; | |||||
685 | } | ||||||
686 | } | ||||||
687 | |||||||
688 | |||||||
689 | |||||||
690 | |||||||
691 | =head1 AUTHOR | ||||||
692 | |||||||
693 | David Precious, C<< |
||||||
694 | |||||||
695 | |||||||
696 | =head1 BUGS / FEATURE REQUESTS | ||||||
697 | |||||||
698 | This is an early version; there may still be bugs present or features missing. | ||||||
699 | |||||||
700 | This is developed on GitHub - please feel free to raise issues or pull requests | ||||||
701 | against the repo at: | ||||||
702 | L |
||||||
703 | |||||||
704 | |||||||
705 | |||||||
706 | =head1 ACKNOWLEDGEMENTS | ||||||
707 | |||||||
708 | Valuable feedback on the early design of this module came from many people, | ||||||
709 | including Matt S Trout (mst), David Golden (xdg), Damien Krotkine (dams), | ||||||
710 | Daniel Perrett, and others. | ||||||
711 | |||||||
712 | Configurable login/logout URLs added by Rene (hertell) | ||||||
713 | |||||||
714 | Regex support for require_role by chenryn | ||||||
715 | |||||||
716 | Support for user_roles looking in other realms by Colin Ewen (casao) | ||||||
717 | |||||||
718 | LDAP provider added by Mark Meyer (ofosos) | ||||||
719 | |||||||
720 | Config options for default login/logout handlers by Henk van Oers (hvoers) | ||||||
721 | |||||||
722 | =head1 LICENSE AND COPYRIGHT | ||||||
723 | |||||||
724 | |||||||
725 | Copyright 2012-13 David Precious. | ||||||
726 | |||||||
727 | This program is free software; you can redistribute it and/or modify it | ||||||
728 | under the terms of either: the GNU General Public License as published | ||||||
729 | by the Free Software Foundation; or the Artistic License. | ||||||
730 | |||||||
731 | See http://dev.perl.org/licenses/ for more information. | ||||||
732 | |||||||
733 | |||||||
734 | =cut | ||||||
735 | |||||||
736 | 1; # End of Dancer::Plugin::Auth::Extensible |