File Coverage

blib/lib/Catalyst/Authentication/Credential/HTTP.pm
Criterion Covered Total %
statement 144 203 70.9
branch 50 98 51.0
condition 7 18 38.8
subroutine 31 32 96.8
pod 9 9 100.0
total 241 360 66.9


line stmt bran cond sub pod time code
1             package Catalyst::Authentication::Credential::HTTP; # git description: 1.016-6-g419d9af
2             # ABSTRACT: HTTP Basic and Digest authentication for Catalyst
3              
4 3     3   3071095 use base qw/Catalyst::Authentication::Credential::Password/;
  3         7  
  3         1291  
5              
6 3     3   578393 use strict;
  3         8  
  3         72  
7 3     3   20 use warnings;
  3         7  
  3         114  
8              
9 3     3   1699 use String::Escape ();
  3         13888  
  3         90  
10 3     3   414 use URI::Escape ();
  3         1105  
  3         46  
11 3     3   778 use Catalyst ();
  3         999853  
  3         47  
12 3     3   17 use Digest::MD5 ();
  3         12  
  3         5202  
13              
14             __PACKAGE__->mk_accessors(qw/
15             _config
16             authorization_required_message
17             password_field
18             username_field
19             type
20             realm
21             algorithm
22             use_uri_for
23             no_unprompted_authorization_required
24             require_ssl
25             broken_dotnet_digest_without_query_string
26             /);
27              
28             our $VERSION = '1.017';
29              
30             sub new {
31 7     7 1 54828 my ($class, $config, $app, $realm) = @_;
32              
33 7   50     52 $config->{username_field} ||= 'username';
34             # _config is shity back-compat with our base class.
35 7 50       56 my $self = { %$config, _config => $config, _debug => $app->debug ? 1 : 0 };
36 7         396 bless $self, $class;
37              
38 7         38 $self->realm($realm);
39              
40 7         2868 $self->init;
41 7         1893 return $self;
42             }
43              
44             sub init {
45 7     7 1 17 my ($self) = @_;
46 7   50     27 my $type = $self->type || 'any';
47              
48 7 50       746 if (!grep /$type/, ('basic', 'digest', 'any')) {
49 0         0 Catalyst::Exception->throw(__PACKAGE__ . " used with unsupported authentication type: " . $type);
50             }
51 7         27 $self->type($type);
52             }
53              
54             sub authenticate {
55 15     15 1 233476 my ( $self, $c, $realm, $auth_info ) = @_;
56 15         34 my $auth;
57              
58 15 100       82 $self->authentication_failed( $c, $realm, $auth_info )
    100          
59             if $self->require_ssl ? $c->req->base->scheme ne 'https' : 0;
60              
61 14 100       34518 $auth = $self->authenticate_digest($c, $realm, $auth_info) if $self->_is_http_auth_type('digest');
62 14 50       37 return $auth if $auth;
63              
64 14 50       41 $auth = $self->authenticate_basic($c, $realm, $auth_info) if $self->_is_http_auth_type('basic');
65 14 100       51 return $auth if $auth;
66              
67 11         56 $self->authentication_failed( $c, $realm, $auth_info );
68             }
69              
70             sub authentication_failed {
71 12     12 1 255 my ( $self, $c, $realm, $auth_info ) = @_;
72 12 100       66 unless ($self->no_unprompted_authorization_required) {
73 11         1289 $self->authorization_required_response($c, $realm, $auth_info);
74 11         82 die $Catalyst::DETACH;
75             }
76             }
77              
78             sub authenticate_basic {
79 14     14 1 38 my ( $self, $c, $realm, $auth_info ) = @_;
80              
81 14 50       72 $c->log->debug('Checking http basic authentication.') if $c->debug;
82              
83 14         723 my $headers = $c->req->headers;
84              
85 14 100       1291 if ( my ( $username, $password ) = $headers->authorization_basic ) {
86 5         248 my $user_obj = $realm->find_user( { $self->username_field => $username }, $c);
87 5 50       1391 if (ref($user_obj)) {
88 5         11 my $opts = {};
89 5 50       24 $opts->{$self->password_field} = $password
90             if $self->password_field;
91 5 100       1059 if ($self->check_password($user_obj, $opts)) {
92 3         1669 return $user_obj;
93             }
94             else {
95 2 50       1112 $c->log->debug("Password mismatch!") if $c->debug;
96 2         113 return;
97             }
98             }
99             else {
100 0 0       0 $c->log->debug("Unable to locate user matching user info provided")
101             if $c->debug;
102 0         0 return;
103             }
104             }
105              
106 9         2562 return;
107             }
108              
109             sub authenticate_digest {
110 11     11 1 27 my ( $self, $c, $realm, $auth_info ) = @_;
111              
112 11 50       67 $c->log->debug('Checking http digest authentication.') if $c->debug;
113              
114 11         722 my $headers = $c->req->headers;
115 11         1174 my @authorization = $headers->header('Authorization');
116 11         394 foreach my $authorization (@authorization) {
117 4 50       16 next unless $authorization =~ m{^Digest};
118             my %res = map {
119 0         0 my @key_val = split /=/, $_, 2;
  0         0  
120 0         0 $key_val[0] = lc $key_val[0];
121 0         0 $key_val[1] =~ s{"}{}g; # remove the quotes
122 0         0 @key_val;
123             } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
124              
125 0         0 my $opaque = $res{opaque};
126 0         0 my $nonce = $self->get_digest_authorization_nonce( $c, __PACKAGE__ . '::opaque:' . $opaque );
127 0 0       0 next unless $nonce;
128              
129 0 0       0 $c->log->debug('Checking authentication parameters.')
130             if $c->debug;
131              
132 0         0 my $uri = $c->request->uri->path_query;
133 0   0     0 my $algorithm = $res{algorithm} || 'MD5';
134 0         0 my $nonce_count = '0x' . $res{nc};
135              
136             my $check = ($uri eq $res{uri} ||
137             ($self->broken_dotnet_digest_without_query_string &&
138             $c->request->uri->path eq $res{uri}))
139             && ( exists $res{username} )
140             && ( exists $res{qop} )
141             && ( exists $res{cnonce} )
142             && ( exists $res{nc} )
143             && $algorithm eq $nonce->algorithm
144             && hex($nonce_count) > hex( $nonce->nonce_count )
145 0   0     0 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
146              
147 0 0       0 unless ($check) {
148 0 0       0 $c->log->debug('Digest authentication failed. Bad request.')
149             if $c->debug;
150 0         0 $c->res->status(400); # bad request
151 0         0 Carp::confess $Catalyst::DETACH;
152             }
153              
154 0 0       0 $c->log->debug('Checking authentication response.')
155             if $c->debug;
156              
157 0         0 my $username = $res{username};
158              
159 0         0 my $user_obj;
160              
161 0 0       0 unless ( $user_obj = $auth_info->{user} ) {
162 0         0 $user_obj = $realm->find_user( { $self->username_field => $username }, $c);
163             }
164 0 0       0 unless ($user_obj) { # no user, no authentication
165 0 0       0 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
166 0         0 return;
167             }
168              
169             # everything looks good, let's check the response
170             # calculate H(A2) as per spec
171 0         0 my $ctx = Digest::MD5->new;
172 0         0 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
173 0 0       0 if ( $res{qop} eq 'auth-int' ) {
174 0         0 my $digest =
175             Digest::MD5::md5_hex( $c->request->body ); # not sure here
176 0         0 $ctx->add( ':', $digest );
177             }
178 0         0 my $A2_digest = $ctx->hexdigest;
179              
180             # the idea of the for loop:
181             # if we do not want to store the plain password in our user store,
182             # we can store md5_hex("$username:$realm:$password") instead
183 0         0 my $password_field = $self->password_field;
184 0         0 for my $r ( 0 .. 1 ) {
185             # calculate H(A1) as per spec
186 0 0       0 my $A1_digest = $r ? $user_obj->$password_field() : do {
187 0         0 $ctx = Digest::MD5->new;
188 0         0 $ctx->add( join( ':', $username, $realm->name, $user_obj->$password_field() ) );
189 0         0 $ctx->hexdigest;
190             };
191 0 0       0 if ( $nonce->algorithm eq 'MD5-sess' ) {
192 0         0 $ctx = Digest::MD5->new;
193 0         0 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
194 0         0 $A1_digest = $ctx->hexdigest;
195             }
196              
197             my $digest_in = join( ':',
198             $A1_digest, $res{nonce},
199 0 0       0 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
200             $A2_digest );
201 0         0 my $rq_digest = Digest::MD5::md5_hex($digest_in);
202 0         0 $nonce->nonce_count($nonce_count);
203 0         0 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
204 0         0 $self->store_digest_authorization_nonce( $c, $key, $nonce );
205 0 0       0 if ($rq_digest eq $res{response}) {
206 0         0 return $user_obj;
207             }
208             }
209             }
210 11         26 return;
211             }
212              
213             sub _check_cache {
214 9     9   16 my $c = shift;
215              
216 9 50       32 die "A cache is needed for http digest authentication."
217             unless $c->can('cache');
218 9         172 return;
219             }
220              
221             sub _is_http_auth_type {
222 50     50   124 my ( $self, $type ) = @_;
223 50         172 my $cfgtype = lc( $self->type );
224 50 100 100     5774 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
225 5         29 return 0;
226             }
227              
228             sub authorization_required_response {
229 11     11 1 30 my ( $self, $c, $realm, $auth_info ) = @_;
230              
231 11         66 $c->res->status(401);
232 11         1318 $c->res->content_type('text/plain');
233 11 100       1426 if (exists $self->{authorization_required_message}) {
234             # If you set the key to undef, don't stamp on the body.
235 2 100       11 $c->res->body($self->authorization_required_message)
236             if defined $self->authorization_required_message;
237             }
238             else {
239 9         36 $c->res->body('Authorization required.');
240             }
241              
242             # *DONT* short circuit
243 11         1219 my $ok;
244 11 100       54 $ok++ if $self->_create_digest_auth_response($c, $auth_info);
245 11 50       53 $ok++ if $self->_create_basic_auth_response($c, $auth_info);
246              
247 11 50       29 unless ( $ok ) {
248 0         0 die 'Could not build authorization required response. '
249             . 'Did you configure a valid authentication http type: '
250             . 'basic, digest, any';
251             }
252 11         23 return;
253             }
254              
255             sub _add_authentication_header {
256 20     20   43 my ( $c, $header ) = @_;
257 20         118 $c->response->headers->push_header( 'WWW-Authenticate' => $header );
258 20         2567 return;
259             }
260              
261             sub _create_digest_auth_response {
262 11     11   27 my ( $self, $c, $opts ) = @_;
263              
264 11 100       2193 return unless $self->_is_http_auth_type('digest');
265              
266 9 50       42 if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
267 9         29 _add_authentication_header( $c, $digest );
268 9         35 return 1;
269             }
270              
271 0         0 return;
272             }
273              
274             sub _create_basic_auth_response {
275 11     11   28 my ( $self, $c, $opts ) = @_;
276              
277 11 50       33 return unless $self->_is_http_auth_type('basic');
278              
279 11 50       49 if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
280 11         35 _add_authentication_header( $c, $basic );
281 11         43 return 1;
282             }
283              
284 0         0 return;
285             }
286              
287             sub _build_auth_header_realm {
288 20     20   40 my ( $self, $c, $opts ) = @_;
289 20 100       96 if ( my $realm_name = String::Escape::qprintable($opts->{realm} ? $opts->{realm} : $self->realm->name) ) {
    50          
290 20 50       3238 $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
291 20         99 return 'realm=' . $realm_name;
292             }
293 0         0 return;
294             }
295              
296             sub _build_auth_header_domain {
297 20     20   47 my ( $self, $c, $opts ) = @_;
298 20 100       68 if ( my $domain = $opts->{domain} ) {
299 4 50 33     25 Catalyst::Exception->throw("domain must be an array reference")
300             unless ref($domain) && ref($domain) eq "ARRAY";
301              
302             my @uris =
303             $self->use_uri_for
304 4         346 ? ( map { $c->uri_for($_) } @$domain )
305 4 100       19 : ( map { URI::Escape::uri_escape($_) } @$domain );
  4         265  
306              
307 4         143 return qq{domain="@uris"};
308             }
309 16         50 return;
310             }
311              
312             sub _build_auth_header_common {
313 20     20   45 my ( $self, $c, $opts ) = @_;
314             return (
315 20         69 $self->_build_auth_header_realm($c, $opts),
316             $self->_build_auth_header_domain($c, $opts),
317             );
318             }
319              
320             sub _build_basic_auth_header {
321 11     11   28 my ( $self, $c, $opts ) = @_;
322 11         33 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
323             }
324              
325             sub _build_digest_auth_header {
326 9     9   22 my ( $self, $c, $opts ) = @_;
327              
328 9         29 my $nonce = $self->_digest_auth_nonce($c, $opts);
329              
330 9         30 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
331              
332 9         85 $self->store_digest_authorization_nonce( $c, $key, $nonce );
333              
334             return _join_auth_header_parts( Digest =>
335             $self->_build_auth_header_common($c, $opts),
336 9         1174 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
  36         218  
337             qop
338             nonce
339             opaque
340             algorithm
341             ),
342             );
343             }
344              
345             sub _digest_auth_nonce {
346 9     9   20 my ( $self, $c, $opts ) = @_;
347              
348 9         18 my $package = __PACKAGE__ . '::Nonce';
349              
350 9         30 my $nonce = $package->new;
351              
352 9 50 33     64 if ( my $algorithm = $opts->{algorithm} || $self->algorithm) {
353 0         0 $nonce->algorithm( $algorithm );
354             }
355              
356 9         1111 return $nonce;
357             }
358              
359             sub _join_auth_header_parts {
360 20     20   96 my ( $type, @parts ) = @_;
361 20         107 return "$type " . join(", ", @parts );
362             }
363              
364             sub get_digest_authorization_nonce {
365 0     0 1 0 my ( $self, $c, $key ) = @_;
366              
367 0         0 _check_cache($c);
368 0         0 return $c->cache->get( $key );
369             }
370              
371             sub store_digest_authorization_nonce {
372 9     9 1 20 my ( $self, $c, $key, $nonce ) = @_;
373              
374 9         29 _check_cache($c);
375 9         46 return $c->cache->set( $key, $nonce );
376             }
377              
378             package # hide from PAUSE
379             Catalyst::Authentication::Credential::HTTP::Nonce;
380              
381 3     3   24 use strict;
  3         7  
  3         88  
382 3     3   16 use base qw[ Class::Accessor::Fast ];
  3         7  
  3         1634  
383 3     3   6868 use Data::UUID 0.11 ();
  3         1550  
  3         263  
384              
385             __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
386              
387             sub new {
388 9     9   17 my $class = shift;
389 9         40 my $self = $class->SUPER::new(@_);
390              
391 9         1680 $self->nonce( Data::UUID->new->create_b64 );
392 9         1117 $self->opaque( Data::UUID->new->create_b64 );
393 9         461 $self->qop('auth,auth-int');
394 9         76 $self->nonce_count('0x0');
395 9         59 $self->algorithm('MD5');
396              
397 9         53 return $self;
398             }
399              
400             1;
401              
402             __END__
403              
404             =pod
405              
406             =encoding UTF-8
407              
408             =head1 NAME
409              
410             Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication for Catalyst
411              
412             =head1 VERSION
413              
414             version 1.017
415              
416             =head1 SYNOPSIS
417              
418             use Catalyst qw/
419             Authentication
420             /;
421              
422             __PACKAGE__->config( authentication => {
423             default_realm => 'example',
424             realms => {
425             example => {
426             credential => {
427             class => 'HTTP',
428             type => 'any', # or 'digest' or 'basic'
429             password_type => 'clear',
430             password_field => 'password'
431             },
432             store => {
433             class => 'Minimal',
434             users => {
435             Mufasa => { password => "Circle Of Life", },
436             },
437             },
438             },
439             }
440             });
441              
442             sub foo : Local {
443             my ( $self, $c ) = @_;
444              
445             $c->authenticate({}, "example");
446             # either user gets authenticated or 401 is sent
447             # Note that the authentication realm sent to the client (in the
448             # RFC 2617 sense) is overridden here, but this *does not*
449             # effect the Catalyst::Authentication::Realm used for
450             # authentication - to do that, you need
451             # $c->authenticate({}, 'otherrealm')
452              
453             do_stuff();
454             }
455              
456             sub always_auth : Local {
457             my ( $self, $c ) = @_;
458              
459             # Force authorization headers onto the response so that the user
460             # is asked again for authentication, even if they successfully
461             # authenticated.
462             my $realm = $c->get_auth_realm('example');
463             $realm->credential->authorization_required_response($c, $realm);
464             }
465              
466             # with ACL plugin
467             __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
468              
469             =head1 DESCRIPTION
470              
471             This module lets you use HTTP authentication with
472             L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
473             are currently supported.
474              
475             When authentication is required, this module sets a status of 401, and
476             the body of the response to 'Authorization required.'. To override
477             this and set your own content, check for the C<< $c->res->status ==
478             401 >> in your C<end> action, and change the body accordingly.
479              
480             =head2 TERMS
481              
482             =over 4
483              
484             =item Nonce
485              
486             A nonce is a one-time value sent with each digest authentication
487             request header. The value must always be unique, so per default the
488             last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
489             change this behaviour, override the
490             C<store_digest_authorization_nonce> and
491             C<get_digest_authorization_nonce> methods as shown below.
492              
493             =back
494              
495             =for stopwords rfc
496             rfc2617
497             auth
498             sess
499              
500             =head1 METHODS
501              
502             =over 4
503              
504             =item new $config, $c, $realm
505              
506             Simple constructor.
507              
508             =item init
509              
510             Validates that $config is ok.
511              
512             =item authenticate $c, $realm, \%auth_info
513              
514             Tries to authenticate the user, and if that fails calls
515             C<authorization_required_response> and detaches the current action call stack.
516              
517             Looks inside C<< $c->request->headers >> and processes the digest and basic
518             (badly named) authorization header.
519              
520             This will only try the methods set in the configuration. First digest, then basic.
521              
522             The %auth_info hash can contain a number of keys which control the authentication behaviour:
523              
524             =over
525              
526             =item realm
527              
528             Sets the HTTP authentication realm presented to the client. Note this does not alter the
529             Catalyst::Authentication::Realm object used for the authentication.
530              
531             =item domain
532              
533             Array reference to domains used to build the authorization headers.
534              
535             This list of domains defines the protection space. If a domain URI is an
536             absolute path (starts with /), it is relative to the root URL of the server being accessed.
537             An absolute URI in this list may refer to a different server than the one being accessed.
538              
539             The client will use this list to determine the set of URIs for which the same authentication
540             information may be sent.
541              
542             If this is omitted or its value is empty, the client will assume that the
543             protection space consists of all URIs on the responding server.
544              
545             Therefore, if your application is not hosted at the root of this domain, and you want to
546             prevent the authentication credentials for this application being sent to any other applications.
547             then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
548              
549             =back
550              
551             =item authenticate_basic $c, $realm, \%auth_info
552              
553             Performs HTTP basic authentication.
554              
555             =item authenticate_digest $c, $realm, \%auth_info
556              
557             Performs HTTP digest authentication.
558              
559             The password_type B<must> be I<clear> for digest authentication to
560             succeed. If you do not want to store your user passwords as clear
561             text, you may instead store the MD5 digest in hex of the string
562             '$username:$realm:$password'.
563              
564             L<Catalyst::Plugin::Cache> is used for persistent storage of the nonce
565             values (see L</Nonce>). It must be loaded in your application, unless
566             you override the C<store_digest_authorization_nonce> and
567             C<get_digest_authorization_nonce> methods as shown below.
568              
569             Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
570             and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
571              
572             =item authorization_required_response $c, $realm, \%auth_info
573              
574             Sets C<< $c->response >> to the correct status code, and adds the correct
575             header to demand authentication data from the user agent.
576              
577             Typically used by C<authenticate>, but may be invoked manually.
578              
579             %opts can contain C<domain> and C<algorithm>, which are used to build
580             %the digest header.
581              
582             =item store_digest_authorization_nonce $c, $key, $nonce
583              
584             =item get_digest_authorization_nonce $c, $key
585              
586             Set or get the C<$nonce> object used by the digest auth mode.
587              
588             You may override these methods. By default they will call C<get> and C<set> on
589             C<< $c->cache >>.
590              
591             =item authentication_failed
592              
593             Sets the 401 response and calls C<< $ctx->detach >>.
594              
595             =back
596              
597             =head1 CONFIGURATION
598              
599             All configuration is stored in C<< YourApp->config('Plugin::Authentication' => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
600              
601             This should be a hash, and it can contain the following entries:
602              
603             =over
604              
605             =item type
606              
607             Can be either C<any> (the default), C<basic> or C<digest>.
608              
609             This controls C<authorization_required_response> and C<authenticate>, but
610             not the "manual" methods.
611              
612             =item authorization_required_message
613              
614             Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
615              
616             =item password_type
617              
618             The type of password returned by the user object. Same usage as in
619             L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type>
620              
621             =item password_field
622              
623             The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
624             L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
625              
626             =item username_field
627              
628             The field name that the user's username is mapped into when finding the user from the realm. Defaults to 'username'.
629              
630             =item use_uri_for
631              
632             If this configuration key has a true value, then the domain(s) for the authorization header will be
633             run through $c->uri_for(). Use this configuration option if your application is not running at the root
634             of your domain, and you want to ensure that authentication credentials from your application are not shared with
635             other applications on the same server.
636              
637             =item require_ssl
638              
639             If this configuration key has a true value then authentication will be denied
640             (and a 401 issued in normal circumstances) unless the request is via https.
641              
642             =item no_unprompted_authorization_required
643              
644             Causes authentication to fail as normal modules do, without calling
645             C<< $c->detach >>. This means that the basic auth credential can be used as
646             part of the progressive realm.
647              
648             However use like this is probably not optimum it also means that users in
649             browsers ill never get a HTTP authenticate dialogue box (unless you manually
650             return a 401 response in your application), and even some automated
651             user agents (for APIs) will not send the Authorization header without
652             specific manipulation of the request headers.
653              
654             =item broken_dotnet_digest_without_query_string
655              
656             Enables support for .NET (or other similarly broken clients), which
657             fails to include the query string in the uri in the digest
658             Authorization header, contrary to rfc2617.
659              
660             This option has no effect on clients that include the query string;
661             they will continue to work as normal.
662              
663             =back
664              
665             =head1 RESTRICTIONS
666              
667             When using digest authentication, this module will only work together
668             with authentication stores whose User objects have a C<password>
669             method that returns the plain-text password. It will not work together
670             with L<Catalyst::Authentication::Store::Htpasswd>, or
671             L<Catalyst::Authentication::Store::DBIC> stores whose
672             C<password> methods return a hashed or salted version of the password.
673              
674             =head1 SEE ALSO
675              
676             RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
677              
678             =head1 SUPPORT
679              
680             Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=Catalyst-Authentication-Credential-HTTP>
681             (or L<bug-Catalyst-Authentication-Credential-HTTP@rt.cpan.org|mailto:bug-Catalyst-Authentication-Credential-HTTP@rt.cpan.org>).
682              
683             There is also a mailing list available for users of this distribution, at
684             L<http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/catalyst>.
685              
686             There is also an irc channel available for users of this distribution, at
687             L<C<#catalyst> on C<irc.perl.org>|irc://irc.perl.org/#catalyst>.
688              
689             =head1 AUTHOR
690              
691             יובל קוג'מן (Yuval Kogman) <nothingmuch@woobling.org>
692              
693             =head1 CONTRIBUTORS
694              
695             =for stopwords Tomas Doran Karen Etheridge Sascha Kiefer Devin Austin Ronald J Kimball Jess Robinson Ton Voon J. Shirley Brian Cassidy Jonathan Rockway
696              
697             =over 4
698              
699             =item *
700              
701             Tomas Doran <bobtfish@bobtfish.net>
702              
703             =item *
704              
705             Karen Etheridge <ether@cpan.org>
706              
707             =item *
708              
709             Sascha Kiefer <esskar@cpan.org>
710              
711             =item *
712              
713             Devin Austin <devin.austin@gmail.com>
714              
715             =item *
716              
717             Ronald J Kimball <rjk@linguist.dartmouth.edu>
718              
719             =item *
720              
721             Jess Robinson <cpan@desert-island.me.uk>
722              
723             =item *
724              
725             Ronald J Kimball <rjk@tamias.net>
726              
727             =item *
728              
729             Tomas Doran <tdoran@yelp.com>
730              
731             =item *
732              
733             Ton Voon <ton.voon@opsera.com>
734              
735             =item *
736              
737             J. Shirley <jshirley+cpan@gmail.com>
738              
739             =item *
740              
741             Brian Cassidy <bricas@cpan.org>
742              
743             =item *
744              
745             Jonathan Rockway <jon@jrock.us>
746              
747             =back
748              
749             =head1 COPYRIGHT AND LICENCE
750              
751             This software is copyright (c) 2006 by יובל קוג'מן (Yuval Kogman).
752              
753             This is free software; you can redistribute it and/or modify it under
754             the same terms as the Perl 5 programming language system itself.
755              
756             =cut