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