Checking in changes prior to tagging of version 1.004. Changelog diff is:
[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
481=back
d99b7693 482
513d8ab6 483=item authenticate_basic $c, $realm, \%auth_info
d99b7693 484
bf399285 485Performs HTTP basic authentication.
490754a8 486
513d8ab6 487=item authenticate_digest $c, $realm, \%auth_info
d99b7693 488
bf399285 489Performs HTTP digest authentication. Note that the password_type B<must> by I<clear> for
c5a1fa88 490digest authentication to succeed, and you must have L<Catalyst::Plugin::Session> in
491your application as digest authentication needs to store persistent data.
492
493Note - if you do not want to store your user passwords as clear text, then it is possible
494to store instead the MD5 digest in hex of the string '$username:$realm:$password'
d99b7693 495
513d8ab6 496=item authorization_required_response $c, $realm, \%auth_info
d99b7693 497
498Sets C<< $c->response >> to the correct status code, and adds the correct
499header to demand authentication data from the user agent.
500
513d8ab6 501Typically used by C<authenticate>, but may be invoked manually.
d99b7693 502
513d8ab6 503%opts can contain C<domain> and C<algorithm>, which are used to build
d99b7693 504%the digest header.
505
513d8ab6 506=item store_digest_authorization_nonce $c, $key, $nonce
d99b7693 507
513d8ab6 508=item get_digest_authorization_nonce $c, $key
d99b7693 509
510Set or get the C<$nonce> object used by the digest auth mode.
511
512You may override these methods. By default they will call C<get> and C<set> on
513C<< $c->cache >>.
514
d99b7693 515=back
516
517=head1 CONFIGURATION
518
513d8ab6 519All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
d99b7693 520
521This should be a hash, and it can contain the following entries:
522
05512a69 523=over
d99b7693 524
d99b7693 525=item type
526
527Can be either C<any> (the default), C<basic> or C<digest>.
528
513d8ab6 529This controls C<authorization_required_response> and C<authenticate>, but
d99b7693 530not the "manual" methods.
531
532=item authorization_required_message
533
513d8ab6 534Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
d99b7693 535
05512a69 536=item password_type
537
f1f73b53 538The type of password returned by the user object. Same usage as in
05512a69 539L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/passwprd_type>
540
541=item password_field
542
f1f73b53 543The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
05512a69 544L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
545
546=item use_uri_for
547
548If this configuration key has a true value, then the domain(s) for the authorization header will be
549run through $c->uri_for()
550
d99b7693 551=back
552
553=head1 RESTRICTIONS
554
555When using digest authentication, this module will only work together
556with authentication stores whose User objects have a C<password>
557method that returns the plain-text password. It will not work together
558with L<Catalyst::Authentication::Store::Htpasswd>, or
513d8ab6 559L<Catalyst::Authentication::Store::DBIC> stores whose
d99b7693 560C<password> methods return a hashed or salted version of the password.
c7b3e379 561
a14203f8 562=head1 AUTHORS
563
513d8ab6 564Updated to current name space and currently maintained
565by: Tomas Doran C<bobtfish@bobtfish.net>.
566
567Original module by:
568
569=over
a14203f8 570
513d8ab6 571=item Yuval Kogman, C<nothingmuch@woobling.org>
a14203f8 572
513d8ab6 573=item Jess Robinson
574
575=item Sascha Kiefer C<esskar@cpan.org>
576
577=back
a14203f8 578
c7b3e379 579=head1 SEE ALSO
580
d99b7693 581RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
c7b3e379 582
a14203f8 583=head1 COPYRIGHT & LICENSE
584
513d8ab6 585 Copyright (c) 2005-2008 the aforementioned authors. All rights
a14203f8 586 reserved. This program is free software; you can redistribute
a14203f8 587 it and/or modify it under the same terms as Perl itself.
588
a14203f8 589=cut
513d8ab6 590