Commit | Line | Data |
06675d2e |
1 | #!/usr/bin/perl |
2 | |
3 | package Catalyst::Plugin::Authentication; |
4 | |
b003080b |
5 | use base qw/Class::Accessor::Fast Class::Data::Inheritable/; |
06675d2e |
6 | |
b003080b |
7 | BEGIN { |
7bb06c91 |
8 | __PACKAGE__->mk_accessors(qw/_user/); |
96777f3a |
9 | __PACKAGE__->mk_classdata($_) for qw/_auth_stores _auth_store_names/; |
b003080b |
10 | } |
06675d2e |
11 | |
12 | use strict; |
13 | use warnings; |
14 | |
96777f3a |
15 | use Tie::RefHash; |
12dae309 |
16 | use Class::Inspector; |
96777f3a |
17 | |
bbf1cb39 |
18 | # this optimization breaks under Template::Toolkit |
19 | # use user_exists instead |
e145babc |
20 | #BEGIN { |
21 | # require constant; |
22 | # constant->import(have_want => eval { require Want }); |
23 | #} |
a1e5bd36 |
24 | |
276b6b76 |
25 | our $VERSION = "0.09"; |
c7c003d3 |
26 | |
06675d2e |
27 | sub set_authenticated { |
28 | my ( $c, $user ) = @_; |
29 | |
30 | $c->user($user); |
e300c5b6 |
31 | $c->request->{user} = $user; # compatibility kludge |
06675d2e |
32 | |
488433fd |
33 | if ( $c->_should_save_user_in_session($user) ) { |
12dae309 |
34 | $c->save_user_in_session($user); |
06675d2e |
35 | } |
55395841 |
36 | |
4fbe2e14 |
37 | $c->NEXT::set_authenticated($user); |
06675d2e |
38 | } |
39 | |
488433fd |
40 | sub _should_save_user_in_session { |
41 | my ( $c, $user ) = @_; |
42 | |
43 | $c->_auth_sessions_supported |
44 | and $c->config->{authentication}{use_session} |
45 | and $user->supports("session"); |
46 | } |
47 | |
48 | sub _should_load_user_from_session { |
49 | my ( $c, $user ) = @_; |
50 | |
51 | $c->_auth_sessions_supported |
52 | and $c->config->{authentication}{use_session} |
53 | and $c->session_is_valid; |
54 | } |
55 | |
56 | sub _auth_sessions_supported { |
57 | my $c = shift; |
58 | $c->isa("Catalyst::Plugin::Session"); |
59 | } |
60 | |
7bb06c91 |
61 | sub user { |
e300c5b6 |
62 | my $c = shift; |
7bb06c91 |
63 | |
e300c5b6 |
64 | if (@_) { |
65 | return $c->_user(@_); |
66 | } |
7bb06c91 |
67 | |
56e23e7a |
68 | if ( defined(my $user = $c->_user) ) { |
69 | return $user; |
70 | } else { |
47c6643f |
71 | return $c->auth_restore_user; |
e300c5b6 |
72 | } |
7bb06c91 |
73 | } |
74 | |
ce0b058d |
75 | sub user_exists { |
76 | my $c = shift; |
9deccb83 |
77 | return defined($c->_user) || defined($c->_user_in_session); |
56e23e7a |
78 | } |
79 | |
12dae309 |
80 | sub save_user_in_session { |
e300c5b6 |
81 | my ( $c, $user ) = @_; |
12dae309 |
82 | |
83 | my $store = $user->store || ref $user; |
84 | $c->session->{__user_store} = $c->get_auth_store_name($store) || $store; |
85 | $c->session->{__user} = $user->for_session; |
86 | } |
87 | |
06675d2e |
88 | sub logout { |
89 | my $c = shift; |
90 | |
91 | $c->user(undef); |
b003080b |
92 | |
488433fd |
93 | if ( $c->_should_load_user_from_session ) { |
94 | $c->_delete_user_from_session(); |
b003080b |
95 | } |
351e2a82 |
96 | |
97 | $c->NEXT::logout(@_); |
06675d2e |
98 | } |
99 | |
488433fd |
100 | sub _delete_user_from_session { |
101 | my $c = shift; |
102 | delete @{ $c->session }{qw/__user __user_store/}; |
103 | } |
104 | |
7d0922d8 |
105 | sub get_user { |
2f7d8b59 |
106 | my ( $c, $uid, @rest ) = @_; |
7d0922d8 |
107 | |
108 | if ( my $store = $c->default_auth_store ) { |
2f7d8b59 |
109 | return $store->get_user( $uid, @rest ); |
7d0922d8 |
110 | } |
111 | else { |
112 | Catalyst::Exception->throw( |
113 | "The user id $uid was passed to an authentication " |
114 | . "plugin, but no default store was specified" ); |
115 | } |
116 | } |
117 | |
47c6643f |
118 | sub _user_in_session { |
119 | my $c = shift; |
120 | |
488433fd |
121 | return unless $c->_should_load_user_from_session; |
47c6643f |
122 | |
123 | return $c->session->{__user}; |
488433fd |
124 | } |
47c6643f |
125 | |
488433fd |
126 | sub _store_in_session { |
127 | my $c = shift; |
128 | |
129 | # we don't need verification, it's only called if _user_in_session returned something useful |
130 | |
131 | return $c->session->{__user_store}; |
47c6643f |
132 | } |
133 | |
7bb06c91 |
134 | sub auth_restore_user { |
e300c5b6 |
135 | my ( $c, $frozen_user, $store_name ) = @_; |
7bb06c91 |
136 | |
47c6643f |
137 | $frozen_user ||= $c->_user_in_session; |
138 | return unless defined($frozen_user); |
4402d92d |
139 | |
488433fd |
140 | $store_name ||= $c->_store_in_session; |
276b6b76 |
141 | return unless $store_name; # FIXME die unless? This is an internal inconsistency |
7bb06c91 |
142 | |
e300c5b6 |
143 | my $store = $c->get_auth_store($store_name); |
488433fd |
144 | |
e300c5b6 |
145 | $c->_user( my $user = $store->from_session( $c, $frozen_user ) ); |
7bb06c91 |
146 | |
e300c5b6 |
147 | return $user; |
7bb06c91 |
148 | |
149 | } |
150 | |
06675d2e |
151 | sub setup { |
152 | my $c = shift; |
153 | |
488433fd |
154 | my $cfg = $c->config->{authentication} ||= {}; |
06675d2e |
155 | |
156 | %$cfg = ( |
157 | use_session => 1, |
158 | %$cfg, |
159 | ); |
b003080b |
160 | |
12dae309 |
161 | $c->register_auth_stores( |
162 | default => $cfg->{store}, |
163 | %{ $cfg->{stores} || {} }, |
164 | ); |
96777f3a |
165 | |
b003080b |
166 | $c->NEXT::setup(@_); |
06675d2e |
167 | } |
168 | |
96777f3a |
169 | sub get_auth_store { |
12dae309 |
170 | my ( $self, $name ) = @_; |
171 | $self->auth_stores->{$name} || ( Class::Inspector->loaded($name) && $name ); |
96777f3a |
172 | } |
173 | |
174 | sub get_auth_store_name { |
12dae309 |
175 | my ( $self, $store ) = @_; |
176 | $self->auth_store_names->{$store}; |
96777f3a |
177 | } |
178 | |
179 | sub register_auth_stores { |
12dae309 |
180 | my ( $self, %new ) = @_; |
96777f3a |
181 | |
12dae309 |
182 | foreach my $name ( keys %new ) { |
183 | my $store = $new{$name} or next; |
184 | $self->auth_stores->{$name} = $store; |
185 | $self->auth_store_names->{$store} = $name; |
186 | } |
96777f3a |
187 | } |
188 | |
189 | sub auth_stores { |
12dae309 |
190 | my $self = shift; |
191 | $self->_auth_stores(@_) || $self->_auth_stores( {} ); |
96777f3a |
192 | } |
193 | |
194 | sub auth_store_names { |
12dae309 |
195 | my $self = shift; |
96777f3a |
196 | |
4402d92d |
197 | $self->_auth_store_names || do { |
12dae309 |
198 | tie my %hash, 'Tie::RefHash'; |
199 | $self->_auth_store_names( \%hash ); |
4fbe2e14 |
200 | } |
96777f3a |
201 | } |
202 | |
203 | sub default_auth_store { |
12dae309 |
204 | my $self = shift; |
96777f3a |
205 | |
12dae309 |
206 | if ( my $new = shift ) { |
207 | $self->register_auth_stores( default => $new ); |
208 | } |
96777f3a |
209 | |
12dae309 |
210 | $self->get_auth_store("default"); |
96777f3a |
211 | } |
212 | |
06675d2e |
213 | __PACKAGE__; |
214 | |
215 | __END__ |
216 | |
217 | =pod |
218 | |
219 | =head1 NAME |
220 | |
55395841 |
221 | Catalyst::Plugin::Authentication - Infrastructure plugin for the Catalyst |
222 | authentication framework. |
06675d2e |
223 | |
224 | =head1 SYNOPSIS |
225 | |
189b5b0c |
226 | use Catalyst qw/ |
227 | Authentication |
228 | Authentication::Store::Foo |
229 | Authentication::Credential::Password |
230 | /; |
231 | |
232 | # later on ... |
233 | # ->login is provided by the Credential::Password module |
234 | $c->login('myusername', 'mypassword'); |
235 | my $age = $c->user->age; |
236 | $c->logout; |
06675d2e |
237 | |
238 | =head1 DESCRIPTION |
239 | |
e7522758 |
240 | The authentication plugin provides generic user support. It is the basis |
241 | for both authentication (checking the user is who they claim to be), and |
242 | authorization (allowing the user to do what the system authorises them to do). |
06675d2e |
243 | |
e7522758 |
244 | Using authentication is split into two parts. A Store is used to actually |
245 | store the user information, and can store any amount of data related to |
246 | the user. Multiple stores can be accessed from within one application. |
247 | Credentials are used to verify users, using the store, given data from |
248 | the frontend. |
189b5b0c |
249 | |
6a36933d |
250 | To implement authentication in a Catalyst application you need to add this |
e7522758 |
251 | module, plus at least one store and one credential module. |
189b5b0c |
252 | |
e7522758 |
253 | Authentication data can also be stored in a session, if the application |
254 | is using the L<Catalyst::Plugin::Session> module. |
06675d2e |
255 | |
4bb9b01c |
256 | =head1 INTRODUCTION |
257 | |
258 | =head2 The Authentication/Authorization Process |
259 | |
260 | Web applications typically need to identify a user - to tell the user apart |
261 | from other users. This is usually done in order to display private information |
262 | that is only that user's business, or to limit access to the application so |
263 | that only certain entities can access certain parts. |
264 | |
265 | This process is split up into several steps. First you ask the user to identify |
266 | themselves. At this point you can't be sure that the user is really who they |
267 | claim to be. |
268 | |
6a36933d |
269 | Then the user tells you who they are, and backs this claim with some piece of |
4bb9b01c |
270 | information that only the real user could give you. For example, a password is |
271 | a secret that is known to both the user and you. When the user tells you this |
272 | password you can assume they're in on the secret and can be trusted (ignore |
273 | identity theft for now). Checking the password, or any other proof is called |
274 | B<credential verification>. |
275 | |
276 | By this time you know exactly who the user is - the user's identity is |
277 | B<authenticated>. This is where this module's job stops, and other plugins step |
278 | in. The next logical step is B<authorization>, the process of deciding what a |
279 | user is (or isn't) allowed to do. For example, say your users are split into |
280 | two main groups - regular users and administrators. You should verify that the |
281 | currently logged in user is indeed an administrator before performing the |
6a36933d |
282 | actions of an administrative part of your application. One way to do this is |
4bb9b01c |
283 | with role based access control. |
284 | |
285 | =head2 The Components In This Framework |
286 | |
287 | =head3 Credential Verifiers |
288 | |
289 | When user input is transferred to the L<Catalyst> application (typically via |
290 | form inputs) this data then enters the authentication framework through these |
291 | plugins. |
292 | |
293 | These plugins check the data, and ensure that it really proves the user is who |
294 | they claim to be. |
295 | |
296 | =head3 Storage Backends |
297 | |
298 | The credentials also identify a user, and this family of modules is supposed to |
299 | take this identification data and return a standardized object oriented |
300 | representation of users. |
301 | |
302 | When a user is retrieved from a store it is not necessarily authenticated. |
303 | Credential verifiers can either accept a user object, or fetch the object |
304 | themselves from the default store. |
305 | |
306 | =head3 The Core Plugin |
307 | |
6a36933d |
308 | This plugin on its own is the glue, providing store registration, session |
4bb9b01c |
309 | integration, and other goodness for the other plugins. |
310 | |
311 | =head3 Other Plugins |
312 | |
313 | More layers of plugins can be stacked on top of the authentication code. For |
314 | example, L<Catalyst::Plugin::Session::PerUser> provides an abstraction of |
315 | browser sessions that is more persistent per users. |
316 | L<Catalyst::Plugin::Authorization::Roles> provides an accepted way to separate |
317 | and group users into categories, and then check which categories the current |
318 | user belongs to. |
319 | |
5e91c057 |
320 | =head1 EXAMPLE |
321 | |
6a36933d |
322 | Let's say we were storing users in an Apache style htpasswd file. Users are |
323 | stored in that file, with a hashed password and some extra comments. Users are |
324 | verified by supplying a password which is matched with the file. |
5e91c057 |
325 | |
326 | This means that our application will begin like this: |
327 | |
328 | package MyApp; |
329 | |
330 | use Catalyst qw/ |
331 | Authentication |
332 | Authentication::Credential::Password |
333 | Authentication::Store::Htpasswd |
334 | /; |
335 | |
336 | __PACKAGE__->config->{authentication}{htpasswd} = "passwdfile"; |
337 | |
338 | This loads the appropriate methods and also sets the htpasswd store as the |
339 | default store. |
340 | |
341 | So, now that we have the code loaded we need to get data from the user into the |
342 | credential verifier. |
343 | |
344 | Let's create an authentication controller: |
345 | |
346 | package MyApp::Controller::Auth; |
347 | |
348 | sub login : Local { |
349 | my ( $self, $c ) = @_; |
350 | |
351 | if ( my $user = $c->req->param("user") |
352 | and my $password = $c->req->param("password") ) |
353 | { |
354 | if ( $c->login( $user, $password ) ) { |
355 | $c->res->body( "hello " . $c->user->name ); |
356 | } else { |
357 | # login incorrect |
358 | } |
359 | } |
360 | else { |
361 | # invalid form input |
362 | } |
363 | } |
364 | |
365 | This code should be very readable. If all the necessary fields are supplied, |
366 | call the L<Authentication::Credential::Password/login> method on the |
367 | controller. If that succeeds the user is logged in. |
368 | |
369 | It could be simplified though: |
370 | |
371 | sub login : Local { |
372 | my ( $self, $c ) = @_; |
373 | |
374 | if ( $c->login ) { |
375 | ... |
376 | } |
377 | } |
378 | |
b840d11c |
379 | Since the C<login> method knows how to find logically named parameters on its |
5e91c057 |
380 | own. |
381 | |
382 | The credential verifier will ask the default store to get the user whose ID is |
383 | the user parameter. In this case the default store is the htpasswd one. Once it |
384 | fetches the user from the store the password is checked and if it's OK |
385 | C<< $c->user >> will contain the user object returned from the htpasswd store. |
386 | |
387 | We can also pass a user object to the credential verifier manually, if we have |
388 | several stores per app. This is discussed in |
389 | L<Catalyst::Plugin::Authentication::Store>. |
390 | |
391 | Now imagine each admin user has a comment set in the htpasswd file saying |
392 | "admin". |
393 | |
394 | A restricted action might look like this: |
395 | |
396 | sub restricted : Local { |
397 | my ( $self, $c ) = @_; |
398 | |
399 | $c->detach("unauthorized") |
400 | unless $c->user_exists |
401 | and $c->user->extra_info() eq "admin"; |
402 | |
403 | # do something restricted here |
404 | } |
405 | |
406 | This is somewhat similar to role based access control. |
407 | L<Catalyst::Plugin::Authentication::Store::Htpasswd> treats the extra info |
408 | field as a comma separated list of roles if it's treated that way. Let's |
409 | leverage this. Add the role authorization plugin: |
410 | |
411 | use Catalyst qw/ |
412 | ... |
413 | Authorization::Roles |
414 | /; |
415 | |
416 | sub restricted : Local { |
417 | my ( $self, $c ) = @_; |
418 | |
a93f1197 |
419 | $c->detach("unauthorized") unless $c->check_roles("admin"); |
5e91c057 |
420 | |
421 | # do something restricted here |
422 | } |
423 | |
424 | This is somewhat simpler and will work if you change your store, too, since the |
425 | role interface is consistent. |
426 | |
427 | Let's say your app grew, and you now have 10000 users. It's no longer efficient |
428 | to maintain an htpasswd file, so you move this data to a database. |
429 | |
430 | use Catalyst qw/ |
431 | Authentication |
432 | Authentication::Credential::Password |
433 | Authentication::Store::DBIC |
434 | Authorization::Roles |
435 | /; |
436 | |
437 | __PACKAGE__->config->{authentication}{dbic} = ...; # see the DBIC store docs |
438 | |
439 | The rest of your code should be unchanged. Now let's say you are integrating |
440 | typekey authentication to your system. For simplicity's sake we'll assume that |
441 | the user's are still keyed in the same way. |
442 | |
443 | use Catalyst qw/ |
444 | Authentication |
445 | Authentication::Credential::Password |
446 | Authentication::Credential::TypeKey |
447 | Authentication::Store::DBIC |
448 | Authorization::Roles |
449 | /; |
450 | |
451 | And in your auth controller add a new action: |
452 | |
453 | sub typekey : Local { |
454 | my ( $self, $c ) = @_; |
455 | |
456 | if ( $c->authenticate_typekey) { # uses $c->req and Authen::TypeKey |
457 | # same stuff as the $c->login method |
458 | # ... |
459 | } |
460 | } |
461 | |
462 | You've now added a new credential verification mechanizm orthogonally to the |
463 | other components. All you have to do is make sure that the credential verifiers |
464 | pass on the same types of parameters to the store in order to retrieve user |
465 | objects. |
466 | |
06675d2e |
467 | =head1 METHODS |
468 | |
469 | =over 4 |
470 | |
06675d2e |
471 | =item user |
472 | |
189b5b0c |
473 | Returns the currently logged in user or undef if there is none. |
06675d2e |
474 | |
ce0b058d |
475 | =item user_exists |
476 | |
477 | Whether or not a user is logged in right now. |
478 | |
8bcb3a4b |
479 | The reason this method exists is that C<< $c->user >> may needlessly load the |
ce0b058d |
480 | user from the auth store. |
481 | |
482 | If you're just going to say |
483 | |
4359cfe3 |
484 | if ( $c->user_exists ) { |
ce0b058d |
485 | # foo |
486 | } else { |
487 | $c->forward("login"); |
488 | } |
489 | |
4359cfe3 |
490 | it should be more efficient than C<< $c->user >> when a user is marked in the |
491 | session but C<< $c->user >> hasn't been called yet. |
ce0b058d |
492 | |
4402d92d |
493 | =item logout |
494 | |
495 | Delete the currently logged in user from C<user> and the session. |
496 | |
7d0922d8 |
497 | =item get_user $uid |
498 | |
189b5b0c |
499 | Fetch a particular users details, defined by the given ID, via the default store. |
500 | |
501 | =back |
502 | |
503 | =head1 CONFIGURATION |
504 | |
505 | =over 4 |
506 | |
507 | =item use_session |
508 | |
509 | Whether or not to store the user's logged in state in the session, if the |
e7522758 |
510 | application is also using the L<Catalyst::Plugin::Session> plugin. This |
511 | value is set to true per default. |
512 | |
513 | =item store |
514 | |
1e055395 |
515 | If multiple stores are being used, set the module you want as default here. |
7d0922d8 |
516 | |
1e055395 |
517 | =item stores |
518 | |
519 | If multiple stores are being used, you need to provide a name for each store |
520 | here, as a hash, the keys are the names you wish to use, and the values are |
521 | the the names of the plugins. |
522 | |
523 | # example |
524 | __PACKAGE__->config( authentication => { |
525 | store => 'Catalyst::Plugin::Authentication::Store::HtPasswd', |
526 | stores => { |
527 | 'dbic' => 'Catalyst::Plugin::Authentication::Store::DBIC' |
528 | } |
529 | }); |
530 | |
2bcde605 |
531 | =back |
1e055395 |
532 | |
4fbe2e14 |
533 | =head1 METHODS FOR STORE MANAGEMENT |
534 | |
fe4cf44a |
535 | =over 4 |
536 | |
7d0922d8 |
537 | =item default_auth_store |
538 | |
4fbe2e14 |
539 | Return the store whose name is 'default'. |
7d0922d8 |
540 | |
189b5b0c |
541 | This is set to C<< $c->config->{authentication}{store} >> if that value exists, |
4fbe2e14 |
542 | or by using a Store plugin: |
543 | |
544 | use Catalyst qw/Authentication Authentication::Store::Minimal/; |
545 | |
546 | Sets the default store to |
547 | L<Catalyst::Plugin::Authentication::Store::Minimal::Backend>. |
548 | |
a1e5bd36 |
549 | |
4fbe2e14 |
550 | =item get_auth_store $name |
551 | |
552 | Return the store whose name is $name. |
553 | |
554 | =item get_auth_store_name $store |
555 | |
556 | Return the name of the store $store. |
557 | |
558 | =item auth_stores |
559 | |
560 | A hash keyed by name, with the stores registered in the app. |
561 | |
562 | =item auth_store_names |
563 | |
564 | A ref-hash keyed by store, which contains the names of the stores. |
565 | |
566 | =item register_auth_stores %stores_by_name |
567 | |
568 | Register stores into the application. |
06675d2e |
569 | |
fe4cf44a |
570 | =back |
571 | |
06675d2e |
572 | =head1 INTERNAL METHODS |
573 | |
574 | =over 4 |
575 | |
576 | =item set_authenticated $user |
577 | |
578 | Marks a user as authenticated. Should be called from a |
579 | C<Catalyst::Plugin::Authentication::Credential> plugin after successful |
580 | authentication. |
581 | |
582 | This involves setting C<user> and the internal data in C<session> if |
583 | L<Catalyst::Plugin::Session> is loaded. |
584 | |
e300c5b6 |
585 | =item auth_restore_user $user |
586 | |
587 | Used to restore a user from the session, by C<user> only when it's actually |
588 | needed. |
589 | |
590 | =item save_user_in_session $user |
591 | |
592 | Used to save the user in a session. |
593 | |
06675d2e |
594 | =item prepare |
595 | |
596 | Revives a user from the session object if there is one. |
597 | |
598 | =item setup |
599 | |
600 | Sets the default configuration parameters. |
601 | |
602 | =item |
603 | |
604 | =back |
605 | |
fbe577ac |
606 | =head1 SEE ALSO |
607 | |
4bb9b01c |
608 | This list might not be up to date. |
609 | |
610 | =head2 User Storage Backends |
611 | |
fbe577ac |
612 | L<Catalyst::Plugin::Authentication::Store::Minimal>, |
4bb9b01c |
613 | L<Catalyst::Plugin::Authentication::Store::Htpasswd>, |
614 | L<Catalyst::Plugin::Authentication::Store::DBIC> (also works with Class::DBI). |
615 | |
616 | =head2 Credential verification |
617 | |
618 | L<Catalyst::Plugin::Authentication::Credential::Password>, |
619 | L<Catalyst::Plugin::Authentication::Credential::HTTP>, |
620 | L<Catalyst::Plugin::Authentication::Credential::TypeKey> |
621 | |
622 | =head2 Authorization |
623 | |
fbe577ac |
624 | L<Catalyst::Plugin::Authorization::ACL>, |
4bb9b01c |
625 | L<Catalyst::Plugin::Authorization::Roles> |
626 | |
5e91c057 |
627 | =head2 Internals Documentation |
628 | |
629 | L<Catalyst::Plugin::Authentication::Store> |
630 | |
4bb9b01c |
631 | =head2 Misc |
632 | |
633 | L<Catalyst::Plugin::Session>, |
634 | L<Catalyst::Plugin::Session::PerUser> |
fbe577ac |
635 | |
93f08fb0 |
636 | =head1 DON'T SEE ALSO |
637 | |
1a05e6ed |
638 | This module along with its sub plugins deprecate a great number of other |
639 | modules. These include L<Catalyst::Plugin::Authentication::Simple>, |
640 | L<Catalyst::Plugin::Authentication::CDBI>. |
93f08fb0 |
641 | |
642 | At the time of writing these plugins have not yet been replaced or updated, but |
1a05e6ed |
643 | should be eventually: L<Catalyst::Plugin::Authentication::OpenID>, |
644 | L<Catalyst::Plugin::Authentication::LDAP>, |
645 | L<Catalyst::Plugin::Authentication::CDBI::Basic>, |
646 | L<Catalyst::Plugin::Authentication::Basic::Remote>. |
93f08fb0 |
647 | |
2bcde605 |
648 | =head1 AUTHORS |
fbe577ac |
649 | |
650 | Yuval Kogman, C<nothingmuch@woobling.org> |
2bcde605 |
651 | |
7d2f34eb |
652 | Jess Robinson |
2bcde605 |
653 | |
7d2f34eb |
654 | David Kamholz |
06675d2e |
655 | |
ff46c00b |
656 | =head1 COPYRIGHT & LICENSE |
fbe577ac |
657 | |
658 | Copyright (c) 2005 the aforementioned authors. All rights |
659 | reserved. This program is free software; you can redistribute |
660 | it and/or modify it under the same terms as Perl itself. |
661 | |
662 | =cut |
06675d2e |
663 | |