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