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