Additional documentation and tests for various small features
[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
490754a8 16our $VERSION = "1.002";
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 ) {
490754a8 146 # FIXME - Do not assume accessor is called password.
d99b7693 147 # calculate H(A1) as per spec
490754a8 148 my $A1_digest = $r ? $user->$password_field() : do {
d99b7693 149 $ctx = Digest::MD5->new;
490754a8 150 $ctx->add( join( ':', $username, $realm->name, $user->$password_field() ) );
d99b7693 151 $ctx->hexdigest;
152 };
153 if ( $nonce->algorithm eq 'MD5-sess' ) {
154 $ctx = Digest::MD5->new;
155 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
156 $A1_digest = $ctx->hexdigest;
157 }
158
513d8ab6 159 my $digest_in = join( ':',
d99b7693 160 $A1_digest, $res{nonce},
161 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
513d8ab6 162 $A2_digest );
163 my $rq_digest = Digest::MD5::md5_hex($digest_in);
d99b7693 164 $nonce->nonce_count($nonce_count);
165 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
166 $nonce );
513d8ab6 167 if ($rq_digest eq $res{response}) {
168 $c->set_authenticated($user);
169 return 1;
170 }
d99b7693 171 }
172 }
513d8ab6 173 return;
d99b7693 174}
175
176sub _check_cache {
177 my $c = shift;
178
179 die "A cache is needed for http digest authentication."
180 unless $c->can('cache');
513d8ab6 181 return;
d99b7693 182}
183
184sub _is_http_auth_type {
513d8ab6 185 my ( $self, $type ) = @_;
186 my $cfgtype = lc( $self->_config->{'type'} || 'any' );
d99b7693 187 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
188 return 0;
189}
190
d99b7693 191sub authorization_required_response {
513d8ab6 192 my ( $self, $c, $realm, $auth_info ) = @_;
d99b7693 193
194 $c->res->status(401);
195 $c->res->content_type('text/plain');
513d8ab6 196 if (exists $self->_config->{authorization_required_message}) {
197 # If you set the key to undef, don't stamp on the body.
198 $c->res->body($self->_config->{authorization_required_message})
199 if defined $c->res->body($self->_config->{authorization_required_message});
200 }
201 else {
202 $c->res->body('Authorization required.');
203 }
d99b7693 204
205 # *DONT* short circuit
206 my $ok;
513d8ab6 207 $ok++ if $self->_create_digest_auth_response($c, $auth_info);
208 $ok++ if $self->_create_basic_auth_response($c, $auth_info);
d99b7693 209
210 unless ( $ok ) {
211 die 'Could not build authorization required response. '
212 . 'Did you configure a valid authentication http type: '
213 . 'basic, digest, any';
214 }
513d8ab6 215 return;
d99b7693 216}
217
218sub _add_authentication_header {
219 my ( $c, $header ) = @_;
513d8ab6 220 $c->response->headers->push_header( 'WWW-Authenticate' => $header );
221 return;
d99b7693 222}
223
224sub _create_digest_auth_response {
513d8ab6 225 my ( $self, $c, $opts ) = @_;
d99b7693 226
513d8ab6 227 return unless $self->_is_http_auth_type('digest');
d99b7693 228
513d8ab6 229 if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
230 _add_authentication_header( $c, $digest );
d99b7693 231 return 1;
232 }
233
234 return;
235}
236
237sub _create_basic_auth_response {
513d8ab6 238 my ( $self, $c, $opts ) = @_;
d99b7693 239
513d8ab6 240 return unless $self->_is_http_auth_type('basic');
d99b7693 241
513d8ab6 242 if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
243 _add_authentication_header( $c, $basic );
d99b7693 244 return 1;
245 }
246
247 return;
248}
249
250sub _build_auth_header_realm {
bf399285 251 my ( $self, $c, $opts ) = @_;
252 if ( my $realm_name = String::Escape::qprintable($opts->{realm} ? $opts->{realm} : $self->realm->name) ) {
513d8ab6 253 $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
254 return 'realm=' . $realm_name;
255 }
256 return;
d99b7693 257}
258
259sub _build_auth_header_domain {
513d8ab6 260 my ( $self, $c, $opts ) = @_;
d99b7693 261
262 if ( my $domain = $opts->{domain} ) {
263 Catalyst::Exception->throw("domain must be an array reference")
264 unless ref($domain) && ref($domain) eq "ARRAY";
265
266 my @uris =
513d8ab6 267 $self->_config->{use_uri_for}
d99b7693 268 ? ( map { $c->uri_for($_) } @$domain )
269 : ( map { URI::Escape::uri_escape($_) } @$domain );
270
271 return qq{domain="@uris"};
513d8ab6 272 }
273 return;
d99b7693 274}
275
276sub _build_auth_header_common {
513d8ab6 277 my ( $self, $c, $opts ) = @_;
d99b7693 278
279 return (
bf399285 280 $self->_build_auth_header_realm($c, $opts),
513d8ab6 281 $self->_build_auth_header_domain($c, $opts),
d99b7693 282 );
283}
284
285sub _build_basic_auth_header {
513d8ab6 286 my ( $self, $c, $opts ) = @_;
287 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
d99b7693 288}
289
290sub _build_digest_auth_header {
513d8ab6 291 my ( $self, $c, $opts ) = @_;
d99b7693 292
513d8ab6 293 my $nonce = $self->_digest_auth_nonce($c, $opts);
d99b7693 294
295 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
296
513d8ab6 297 $self->store_digest_authorization_nonce( $c, $key, $nonce );
a14203f8 298
513d8ab6 299 return _join_auth_header_parts( Digest =>
300 $self->_build_auth_header_common($c, $opts),
d99b7693 301 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
302 qop
303 nonce
304 opaque
305 algorithm
306 ),
307 );
308}
a14203f8 309
d99b7693 310sub _digest_auth_nonce {
513d8ab6 311 my ( $self, $c, $opts ) = @_;
d99b7693 312
313 my $package = __PACKAGE__ . '::Nonce';
314
315 my $nonce = $package->new;
316
513d8ab6 317 if ( my $algorithm = $opts->{algorithm} || $self->_config->{algorithm}) {
d99b7693 318 $nonce->algorithm( $algorithm );
319 }
320
321 return $nonce;
322}
323
324sub _join_auth_header_parts {
513d8ab6 325 my ( $type, @parts ) = @_;
d99b7693 326 return "$type " . join(", ", @parts );
327}
328
329sub get_digest_authorization_nonce {
513d8ab6 330 my ( $self, $c, $key ) = @_;
331
332 _check_cache($c);
333 return $c->cache->get( $key );
d99b7693 334}
335
336sub store_digest_authorization_nonce {
513d8ab6 337 my ( $self, $c, $key, $nonce ) = @_;
d99b7693 338
513d8ab6 339 _check_cache($c);
340 return $c->cache->set( $key, $nonce );
d99b7693 341}
342
513d8ab6 343package Catalyst::Authentication::Credential::HTTP::Nonce;
d99b7693 344
345use strict;
346use base qw[ Class::Accessor::Fast ];
347use Data::UUID ();
348
513d8ab6 349our $VERSION = '0.02';
d99b7693 350
351__PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
352
353sub new {
354 my $class = shift;
355 my $self = $class->SUPER::new(@_);
356
357 $self->nonce( Data::UUID->new->create_b64 );
358 $self->opaque( Data::UUID->new->create_b64 );
359 $self->qop('auth,auth-int');
360 $self->nonce_count('0x0');
361 $self->algorithm('MD5');
362
363 return $self;
364}
a14203f8 365
a14203f8 3661;
367
a14203f8 368__END__
369
a14203f8 370=pod
371
a14203f8 372=head1 NAME
373
513d8ab6 374Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
53306b93 375for Catalyst.
a14203f8 376
a14203f8 377=head1 SYNOPSIS
378
a14203f8 379 use Catalyst qw/
a14203f8 380 Authentication
a14203f8 381 /;
382
513d8ab6 383 __PACKAGE__->config( authentication => {
384 realms => {
385 example => {
386 credential => {
387 class => 'HTTP',
388 type => 'any', # or 'digest' or 'basic'
490754a8 389 password_type => 'clear',
390 password_field => 'password'
513d8ab6 391 },
392 store => {
393 class => 'Minimal',
394 users => {
395 Mufasa => { password => "Circle Of Life", },
396 },
397 },
398 },
399 }
400 });
d99b7693 401
402 sub foo : Local {
403 my ( $self, $c ) = @_;
404
513d8ab6 405 $c->authenticate({ realm => "example" });
d99b7693 406 # either user gets authenticated or 401 is sent
bf399285 407 # Note that the authentication realm sent to the client is overridden
408 # here, but this does not affect the Catalyst::Authentication::Realm
409 # used for authentication.
d99b7693 410
411 do_stuff();
412 }
031f556c 413
414 sub always_auth : Local {
415 my ( $self, $c ) = @_;
416
417 # Force authorization headers onto the response so that the user
418 # is asked again for authentication, even if they successfully
419 # authenticated.
420 my $realm = $c->get_auth_realm('example');
421 $realm->credential->authorization_required_response($c, $realm);
422 }
d99b7693 423
424 # with ACL plugin
513d8ab6 425 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
d99b7693 426
a14203f8 427=head1 DESCRIPTION
428
513d8ab6 429This module lets you use HTTP authentication with
d99b7693 430L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
431are currently supported.
432
433When authentication is required, this module sets a status of 401, and
434the body of the response to 'Authorization required.'. To override
435this and set your own content, check for the C<< $c->res->status ==
436401 >> in your C<end> action, and change the body accordingly.
437
438=head2 TERMS
439
440=over 4
441
442=item Nonce
443
444A nonce is a one-time value sent with each digest authentication
445request header. The value must always be unique, so per default the
446last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
447change this behaviour, override the
448C<store_digest_authorization_nonce> and
449C<get_digest_authorization_nonce> methods as shown below.
450
451=back
452
453=head1 METHODS
454
455=over 4
456
513d8ab6 457=item new $config, $c, $realm
d99b7693 458
513d8ab6 459Simple constructor.
d99b7693 460
513d8ab6 461=item authenticate $c, $realm, \%auth_info
d99b7693 462
513d8ab6 463Tries to authenticate the user, and if that fails calls
464C<authorization_required_response> and detaches the current action call stack.
d99b7693 465
466Looks inside C<< $c->request->headers >> and processes the digest and basic
467(badly named) authorization header.
468
469This will only try the methods set in the configuration. First digest, then basic.
470
bf399285 471The %auth_info hash can contain a number of keys which control the authentication behaviour:
472
473=over
474
475=item realm
476
477Sets the HTTP authentication realm presented to the client. Note this does not alter the
478Catalyst::Authentication::Realm object used for the authentication.
479
05512a69 480=item domain
bf399285 481
05512a69 482Array reference to domains used to build the authorization headers.
bf399285 483
484=back
d99b7693 485
513d8ab6 486=item authenticate_basic $c, $realm, \%auth_info
d99b7693 487
bf399285 488Performs HTTP basic authentication.
490754a8 489
513d8ab6 490=item authenticate_digest $c, $realm, \%auth_info
d99b7693 491
bf399285 492Performs HTTP digest authentication. Note that the password_type B<must> by I<clear> for
493digest authentication to succeed.
d99b7693 494
513d8ab6 495=item authorization_required_response $c, $realm, \%auth_info
d99b7693 496
497Sets C<< $c->response >> to the correct status code, and adds the correct
498header to demand authentication data from the user agent.
499
513d8ab6 500Typically used by C<authenticate>, but may be invoked manually.
d99b7693 501
513d8ab6 502%opts can contain C<domain> and C<algorithm>, which are used to build
d99b7693 503%the digest header.
504
513d8ab6 505=item store_digest_authorization_nonce $c, $key, $nonce
d99b7693 506
513d8ab6 507=item get_digest_authorization_nonce $c, $key
d99b7693 508
509Set or get the C<$nonce> object used by the digest auth mode.
510
511You may override these methods. By default they will call C<get> and C<set> on
512C<< $c->cache >>.
513
d99b7693 514=back
515
516=head1 CONFIGURATION
517
513d8ab6 518All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
d99b7693 519
520This should be a hash, and it can contain the following entries:
521
05512a69 522=over
d99b7693 523
d99b7693 524=item type
525
526Can be either C<any> (the default), C<basic> or C<digest>.
527
513d8ab6 528This controls C<authorization_required_response> and C<authenticate>, but
d99b7693 529not the "manual" methods.
530
531=item authorization_required_message
532
513d8ab6 533Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
d99b7693 534
05512a69 535=item password_type
536
537The type of password returned by the user object. Same useage as in
538L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/passwprd_type>
539
540=item password_field
541
542The name of accessor used to retrieve the value of the password field from the user object. Same useage as in
543L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
544
545=item use_uri_for
546
547If this configuration key has a true value, then the domain(s) for the authorization header will be
548run through $c->uri_for()
549
d99b7693 550=back
551
552=head1 RESTRICTIONS
553
554When using digest authentication, this module will only work together
555with authentication stores whose User objects have a C<password>
556method that returns the plain-text password. It will not work together
557with L<Catalyst::Authentication::Store::Htpasswd>, or
513d8ab6 558L<Catalyst::Authentication::Store::DBIC> stores whose
d99b7693 559C<password> methods return a hashed or salted version of the password.
c7b3e379 560
a14203f8 561=head1 AUTHORS
562
513d8ab6 563Updated to current name space and currently maintained
564by: Tomas Doran C<bobtfish@bobtfish.net>.
565
566Original module by:
567
568=over
a14203f8 569
513d8ab6 570=item Yuval Kogman, C<nothingmuch@woobling.org>
a14203f8 571
513d8ab6 572=item Jess Robinson
573
574=item Sascha Kiefer C<esskar@cpan.org>
575
576=back
a14203f8 577
c7b3e379 578=head1 SEE ALSO
579
d99b7693 580RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
c7b3e379 581
a14203f8 582=head1 COPYRIGHT & LICENSE
583
513d8ab6 584 Copyright (c) 2005-2008 the aforementioned authors. All rights
a14203f8 585 reserved. This program is free software; you can redistribute
a14203f8 586 it and/or modify it under the same terms as Perl itself.
587
a14203f8 588=cut
513d8ab6 589