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