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