Commit | Line | Data |
513d8ab6 |
1 | package Catalyst::Authentication::Credential::HTTP; |
490754a8 |
2 | use base qw/Catalyst::Authentication::Credential::Password/; |
d99b7693 |
3 | |
4 | use strict; |
5 | use warnings; |
6 | |
7 | use String::Escape (); |
8 | use URI::Escape (); |
9 | use Catalyst (); |
10 | use 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 | |
9fd0d1fb |
26 | our $VERSION = '1.014'; |
d99b7693 |
27 | |
513d8ab6 |
28 | sub 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 | |
42 | sub 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 |
52 | sub 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 | |
68 | sub 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 | |
76 | sub 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 | |
107 | sub 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 | |
211 | sub _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 | |
219 | sub _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 |
226 | sub 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 | |
253 | sub _add_authentication_header { |
254 | my ( $c, $header ) = @_; |
513d8ab6 |
255 | $c->response->headers->push_header( 'WWW-Authenticate' => $header ); |
256 | return; |
d99b7693 |
257 | } |
258 | |
259 | sub _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 | |
272 | sub _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 | |
285 | sub _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 | |
294 | sub _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 | |
310 | sub _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 | |
318 | sub _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 | |
323 | sub _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 |
343 | sub _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 | |
357 | sub _join_auth_header_parts { |
513d8ab6 |
358 | my ( $type, @parts ) = @_; |
d99b7693 |
359 | return "$type " . join(", ", @parts ); |
360 | } |
361 | |
362 | sub 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 | |
369 | sub 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 |
376 | package Catalyst::Authentication::Credential::HTTP::Nonce; |
d99b7693 |
377 | |
378 | use strict; |
379 | use base qw[ Class::Accessor::Fast ]; |
380 | use Data::UUID (); |
381 | |
513d8ab6 |
382 | our $VERSION = '0.02'; |
d99b7693 |
383 | |
384 | __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]); |
385 | |
386 | sub 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 |
399 | 1; |
400 | |
a14203f8 |
401 | __END__ |
402 | |
a14203f8 |
403 | =pod |
404 | |
a14203f8 |
405 | =head1 NAME |
406 | |
513d8ab6 |
407 | Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication |
53306b93 |
408 | for 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 |
465 | This module lets you use HTTP authentication with |
d99b7693 |
466 | L<Catalyst::Plugin::Authentication>. Both basic and digest authentication |
467 | are currently supported. |
468 | |
469 | When authentication is required, this module sets a status of 401, and |
470 | the body of the response to 'Authorization required.'. To override |
471 | this and set your own content, check for the C<< $c->res->status == |
472 | 401 >> in your C<end> action, and change the body accordingly. |
473 | |
474 | =head2 TERMS |
475 | |
476 | =over 4 |
477 | |
478 | =item Nonce |
479 | |
480 | A nonce is a one-time value sent with each digest authentication |
481 | request header. The value must always be unique, so per default the |
482 | last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To |
483 | change this behaviour, override the |
484 | C<store_digest_authorization_nonce> and |
485 | C<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 |
495 | Simple constructor. |
d99b7693 |
496 | |
41091cd6 |
497 | =item init |
498 | |
499 | Validates that $config is ok. |
500 | |
513d8ab6 |
501 | =item authenticate $c, $realm, \%auth_info |
d99b7693 |
502 | |
513d8ab6 |
503 | Tries to authenticate the user, and if that fails calls |
504 | C<authorization_required_response> and detaches the current action call stack. |
d99b7693 |
505 | |
506 | Looks inside C<< $c->request->headers >> and processes the digest and basic |
507 | (badly named) authorization header. |
508 | |
509 | This will only try the methods set in the configuration. First digest, then basic. |
510 | |
bf399285 |
511 | The %auth_info hash can contain a number of keys which control the authentication behaviour: |
512 | |
513 | =over |
514 | |
515 | =item realm |
516 | |
517 | Sets the HTTP authentication realm presented to the client. Note this does not alter the |
518 | Catalyst::Authentication::Realm object used for the authentication. |
519 | |
05512a69 |
520 | =item domain |
bf399285 |
521 | |
05512a69 |
522 | Array reference to domains used to build the authorization headers. |
bf399285 |
523 | |
61d22a88 |
524 | This list of domains defines the protection space. If a domain URI is an |
525 | absolute path (starts with /), it is relative to the root URL of the server being accessed. |
526 | An absolute URI in this list may refer to a different server than the one being accessed. |
ea92acf7 |
527 | |
61d22a88 |
528 | The client will use this list to determine the set of URIs for which the same authentication |
529 | information may be sent. |
ea92acf7 |
530 | |
531 | If this is omitted or its value is empty, the client will assume that the |
532 | protection space consists of all URIs on the responding server. |
533 | |
534 | Therefore, if your application is not hosted at the root of this domain, and you want to |
535 | prevent the authentication credentials for this application being sent to any other applications. |
536 | then 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 |
542 | Performs HTTP basic authentication. |
490754a8 |
543 | |
513d8ab6 |
544 | =item authenticate_digest $c, $realm, \%auth_info |
d99b7693 |
545 | |
1cd102dc |
546 | Performs HTTP digest authentication. |
c5a1fa88 |
547 | |
1cd102dc |
548 | The password_type B<must> be I<clear> for digest authentication to |
549 | succeed. If you do not want to store your user passwords as clear |
550 | text, you may instead store the MD5 digest in hex of the string |
551 | '$username:$realm:$password'. |
552 | |
553 | L<Catalyst::Plugin::Cache> is used for persistent storage of the nonce |
554 | values (see L</Nonce>). It must be loaded in your application, unless |
555 | you override the C<store_digest_authorization_nonce> and |
556 | C<get_digest_authorization_nonce> methods as shown below. |
d99b7693 |
557 | |
ea92acf7 |
558 | Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default) |
559 | and '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 | |
563 | Sets C<< $c->response >> to the correct status code, and adds the correct |
564 | header to demand authentication data from the user agent. |
565 | |
513d8ab6 |
566 | Typically 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 | |
575 | Set or get the C<$nonce> object used by the digest auth mode. |
576 | |
577 | You may override these methods. By default they will call C<get> and C<set> on |
578 | C<< $c->cache >>. |
579 | |
8ab131f6 |
580 | =item authentication_failed |
581 | |
582 | Sets the 401 response and calls C<< $ctx->detach >>. |
583 | |
d99b7693 |
584 | =back |
585 | |
586 | =head1 CONFIGURATION |
587 | |
282361af |
588 | All configuration is stored in C<< YourApp->config('Plugin::Authentication' => { yourrealm => { credential => { class => 'HTTP', %config } } } >>. |
d99b7693 |
589 | |
590 | This should be a hash, and it can contain the following entries: |
591 | |
05512a69 |
592 | =over |
d99b7693 |
593 | |
d99b7693 |
594 | =item type |
595 | |
596 | Can be either C<any> (the default), C<basic> or C<digest>. |
597 | |
513d8ab6 |
598 | This controls C<authorization_required_response> and C<authenticate>, but |
d99b7693 |
599 | not the "manual" methods. |
600 | |
601 | =item authorization_required_message |
602 | |
513d8ab6 |
603 | Set 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 |
607 | The type of password returned by the user object. Same usage as in |
b56120cd |
608 | L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type> |
05512a69 |
609 | |
610 | =item password_field |
611 | |
61d22a88 |
612 | The name of accessor used to retrieve the value of the password field from the user object. Same usage as in |
05512a69 |
613 | L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field> |
614 | |
a50635bf |
615 | =item username_field |
616 | |
617 | The 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 | |
621 | If this configuration key has a true value, then the domain(s) for the authorization header will be |
ea92acf7 |
622 | run through $c->uri_for(). Use this configuration option if your application is not running at the root |
623 | of your domain, and you want to ensure that authentication credentials from your application are not shared with |
624 | other applications on the same server. |
05512a69 |
625 | |
282361af |
626 | =item require_ssl |
627 | |
628 | If 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 | |
633 | Causes authentication to fail as normal modules do, without calling |
634 | C<< $c->detach >>. This means that the basic auth credential can be used as |
635 | part of the progressive realm. |
636 | |
637 | However use like this is probably not optimum it also means that users in |
638 | browsers ill never get a HTTP authenticate dialogue box (unless you manually |
ae265059 |
639 | return a 401 response in your application), and even some automated |
282361af |
640 | user agents (for APIs) will not send the Authorization header without |
641 | specific manipulation of the request headers. |
642 | |
47a916e2 |
643 | =item broken_dotnet_digest_without_query_string |
644 | |
645 | Enables support for .NET (or other similarly broken clients), which |
646 | fails to include the query string in the uri in the digest |
656e911d |
647 | Authorization header, contrary to rfc2617. |
47a916e2 |
648 | |
649 | This option has no effect on clients that include the query string; |
650 | they will continue to work as normal. |
651 | |
d99b7693 |
652 | =back |
653 | |
654 | =head1 RESTRICTIONS |
655 | |
656 | When using digest authentication, this module will only work together |
657 | with authentication stores whose User objects have a C<password> |
658 | method that returns the plain-text password. It will not work together |
659 | with L<Catalyst::Authentication::Store::Htpasswd>, or |
513d8ab6 |
660 | L<Catalyst::Authentication::Store::DBIC> stores whose |
d99b7693 |
661 | C<password> methods return a hashed or salted version of the password. |
c7b3e379 |
662 | |
a14203f8 |
663 | =head1 AUTHORS |
664 | |
513d8ab6 |
665 | Updated to current name space and currently maintained |
666 | by: Tomas Doran C<bobtfish@bobtfish.net>. |
667 | |
2dad9ca6 |
668 | Original 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 | |
682 | Patches 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 |
696 | RFC 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 | |