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