remove $VERSION from secondary package
[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
d99b7693 382__PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
383
384sub new {
385 my $class = shift;
386 my $self = $class->SUPER::new(@_);
387
388 $self->nonce( Data::UUID->new->create_b64 );
389 $self->opaque( Data::UUID->new->create_b64 );
390 $self->qop('auth,auth-int');
391 $self->nonce_count('0x0');
392 $self->algorithm('MD5');
393
394 return $self;
395}
a14203f8 396
a14203f8 3971;
398
a14203f8 399__END__
400
a14203f8 401=pod
402
a14203f8 403=head1 NAME
404
513d8ab6 405Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
53306b93 406for Catalyst.
a14203f8 407
a14203f8 408=head1 SYNOPSIS
409
a14203f8 410 use Catalyst qw/
a14203f8 411 Authentication
a14203f8 412 /;
413
513d8ab6 414 __PACKAGE__->config( authentication => {
5a73fc8d 415 default_realm => 'example',
61d22a88 416 realms => {
417 example => {
418 credential => {
513d8ab6 419 class => 'HTTP',
420 type => 'any', # or 'digest' or 'basic'
490754a8 421 password_type => 'clear',
422 password_field => 'password'
513d8ab6 423 },
424 store => {
425 class => 'Minimal',
426 users => {
427 Mufasa => { password => "Circle Of Life", },
428 },
429 },
430 },
431 }
432 });
d99b7693 433
434 sub foo : Local {
435 my ( $self, $c ) = @_;
436
9a901542 437 $c->authenticate({}, "example");
d99b7693 438 # either user gets authenticated or 401 is sent
61d22a88 439 # Note that the authentication realm sent to the client (in the
440 # RFC 2617 sense) is overridden here, but this *does not*
441 # effect the Catalyst::Authentication::Realm used for
442 # authentication - to do that, you need
2101d025 443 # $c->authenticate({}, 'otherrealm')
d99b7693 444
445 do_stuff();
446 }
61d22a88 447
031f556c 448 sub always_auth : Local {
449 my ( $self, $c ) = @_;
61d22a88 450
031f556c 451 # Force authorization headers onto the response so that the user
452 # is asked again for authentication, even if they successfully
453 # authenticated.
454 my $realm = $c->get_auth_realm('example');
455 $realm->credential->authorization_required_response($c, $realm);
456 }
d99b7693 457
458 # with ACL plugin
513d8ab6 459 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
d99b7693 460
a14203f8 461=head1 DESCRIPTION
462
513d8ab6 463This module lets you use HTTP authentication with
d99b7693 464L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
465are currently supported.
466
467When authentication is required, this module sets a status of 401, and
468the body of the response to 'Authorization required.'. To override
469this and set your own content, check for the C<< $c->res->status ==
470401 >> in your C<end> action, and change the body accordingly.
471
472=head2 TERMS
473
474=over 4
475
476=item Nonce
477
478A nonce is a one-time value sent with each digest authentication
479request header. The value must always be unique, so per default the
480last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
481change this behaviour, override the
482C<store_digest_authorization_nonce> and
483C<get_digest_authorization_nonce> methods as shown below.
484
485=back
486
487=head1 METHODS
488
489=over 4
490
513d8ab6 491=item new $config, $c, $realm
d99b7693 492
513d8ab6 493Simple constructor.
d99b7693 494
41091cd6 495=item init
496
497Validates that $config is ok.
498
513d8ab6 499=item authenticate $c, $realm, \%auth_info
d99b7693 500
513d8ab6 501Tries to authenticate the user, and if that fails calls
502C<authorization_required_response> and detaches the current action call stack.
d99b7693 503
504Looks inside C<< $c->request->headers >> and processes the digest and basic
505(badly named) authorization header.
506
507This will only try the methods set in the configuration. First digest, then basic.
508
bf399285 509The %auth_info hash can contain a number of keys which control the authentication behaviour:
510
511=over
512
513=item realm
514
515Sets the HTTP authentication realm presented to the client. Note this does not alter the
516Catalyst::Authentication::Realm object used for the authentication.
517
05512a69 518=item domain
bf399285 519
05512a69 520Array reference to domains used to build the authorization headers.
bf399285 521
61d22a88 522This list of domains defines the protection space. If a domain URI is an
523absolute path (starts with /), it is relative to the root URL of the server being accessed.
524An absolute URI in this list may refer to a different server than the one being accessed.
ea92acf7 525
61d22a88 526The client will use this list to determine the set of URIs for which the same authentication
527information may be sent.
ea92acf7 528
529If this is omitted or its value is empty, the client will assume that the
530protection space consists of all URIs on the responding server.
531
532Therefore, if your application is not hosted at the root of this domain, and you want to
533prevent the authentication credentials for this application being sent to any other applications.
534then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
535
bf399285 536=back
d99b7693 537
513d8ab6 538=item authenticate_basic $c, $realm, \%auth_info
d99b7693 539
bf399285 540Performs HTTP basic authentication.
490754a8 541
513d8ab6 542=item authenticate_digest $c, $realm, \%auth_info
d99b7693 543
1cd102dc 544Performs HTTP digest authentication.
c5a1fa88 545
1cd102dc 546The password_type B<must> be I<clear> for digest authentication to
547succeed. If you do not want to store your user passwords as clear
548text, you may instead store the MD5 digest in hex of the string
549'$username:$realm:$password'.
550
551L<Catalyst::Plugin::Cache> is used for persistent storage of the nonce
552values (see L</Nonce>). It must be loaded in your application, unless
553you override the C<store_digest_authorization_nonce> and
554C<get_digest_authorization_nonce> methods as shown below.
d99b7693 555
ea92acf7 556Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
557and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
558
513d8ab6 559=item authorization_required_response $c, $realm, \%auth_info
d99b7693 560
561Sets C<< $c->response >> to the correct status code, and adds the correct
562header to demand authentication data from the user agent.
563
513d8ab6 564Typically used by C<authenticate>, but may be invoked manually.
d99b7693 565
513d8ab6 566%opts can contain C<domain> and C<algorithm>, which are used to build
d99b7693 567%the digest header.
568
513d8ab6 569=item store_digest_authorization_nonce $c, $key, $nonce
d99b7693 570
513d8ab6 571=item get_digest_authorization_nonce $c, $key
d99b7693 572
573Set or get the C<$nonce> object used by the digest auth mode.
574
575You may override these methods. By default they will call C<get> and C<set> on
576C<< $c->cache >>.
577
8ab131f6 578=item authentication_failed
579
580Sets the 401 response and calls C<< $ctx->detach >>.
581
d99b7693 582=back
583
584=head1 CONFIGURATION
585
282361af 586All configuration is stored in C<< YourApp->config('Plugin::Authentication' => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
d99b7693 587
588This should be a hash, and it can contain the following entries:
589
05512a69 590=over
d99b7693 591
d99b7693 592=item type
593
594Can be either C<any> (the default), C<basic> or C<digest>.
595
513d8ab6 596This controls C<authorization_required_response> and C<authenticate>, but
d99b7693 597not the "manual" methods.
598
599=item authorization_required_message
600
513d8ab6 601Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
d99b7693 602
05512a69 603=item password_type
604
61d22a88 605The type of password returned by the user object. Same usage as in
b56120cd 606L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type>
05512a69 607
608=item password_field
609
61d22a88 610The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
05512a69 611L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
612
a50635bf 613=item username_field
614
615The field name that the user's username is mapped into when finding the user from the realm. Defaults to 'username'.
616
05512a69 617=item use_uri_for
618
619If this configuration key has a true value, then the domain(s) for the authorization header will be
ea92acf7 620run through $c->uri_for(). Use this configuration option if your application is not running at the root
621of your domain, and you want to ensure that authentication credentials from your application are not shared with
622other applications on the same server.
05512a69 623
282361af 624=item require_ssl
625
626If this configuration key has a true value then authentication will be denied
627(and a 401 issued in normal circumstances) unless the request is via https.
628
629=item no_unprompted_authorization_required
630
631Causes authentication to fail as normal modules do, without calling
632C<< $c->detach >>. This means that the basic auth credential can be used as
633part of the progressive realm.
634
635However use like this is probably not optimum it also means that users in
636browsers ill never get a HTTP authenticate dialogue box (unless you manually
ae265059 637return a 401 response in your application), and even some automated
282361af 638user agents (for APIs) will not send the Authorization header without
639specific manipulation of the request headers.
640
47a916e2 641=item broken_dotnet_digest_without_query_string
642
643Enables support for .NET (or other similarly broken clients), which
644fails to include the query string in the uri in the digest
656e911d 645Authorization header, contrary to rfc2617.
47a916e2 646
647This option has no effect on clients that include the query string;
648they will continue to work as normal.
649
d99b7693 650=back
651
652=head1 RESTRICTIONS
653
654When using digest authentication, this module will only work together
655with authentication stores whose User objects have a C<password>
656method that returns the plain-text password. It will not work together
657with L<Catalyst::Authentication::Store::Htpasswd>, or
513d8ab6 658L<Catalyst::Authentication::Store::DBIC> stores whose
d99b7693 659C<password> methods return a hashed or salted version of the password.
c7b3e379 660
a14203f8 661=head1 AUTHORS
662
513d8ab6 663Updated to current name space and currently maintained
664by: Tomas Doran C<bobtfish@bobtfish.net>.
665
2dad9ca6 666Original module by:
513d8ab6 667
668=over
a14203f8 669
513d8ab6 670=item Yuval Kogman, C<nothingmuch@woobling.org>
a14203f8 671
513d8ab6 672=item Jess Robinson
673
674=item Sascha Kiefer C<esskar@cpan.org>
675
676=back
a14203f8 677
2dad9ca6 678=head1 CONTRIBUTORS
679
680Patches contributed by:
681
682=over
683
684=item Peter Corlett
685
de3a252c 686=item Devin Austin (dhoss) C<dhoss@cpan.org>
687
47a916e2 688=item Ronald J Kimball
689
2dad9ca6 690=back
691
c7b3e379 692=head1 SEE ALSO
693
d99b7693 694RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
c7b3e379 695
a14203f8 696=head1 COPYRIGHT & LICENSE
697
513d8ab6 698 Copyright (c) 2005-2008 the aforementioned authors. All rights
a14203f8 699 reserved. This program is free software; you can redistribute
a14203f8 700 it and/or modify it under the same terms as Perl itself.
701
a14203f8 702=cut
513d8ab6 703