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