Fix a load of the documentation up. Fix overriding HTTP auth relam in the ->authentic...
[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 }
413
414 # with ACL plugin
513d8ab6 415 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
d99b7693 416
a14203f8 417=head1 DESCRIPTION
418
513d8ab6 419This module lets you use HTTP authentication with
d99b7693 420L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
421are currently supported.
422
423When authentication is required, this module sets a status of 401, and
424the body of the response to 'Authorization required.'. To override
425this and set your own content, check for the C<< $c->res->status ==
426401 >> in your C<end> action, and change the body accordingly.
427
428=head2 TERMS
429
430=over 4
431
432=item Nonce
433
434A nonce is a one-time value sent with each digest authentication
435request header. The value must always be unique, so per default the
436last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
437change this behaviour, override the
438C<store_digest_authorization_nonce> and
439C<get_digest_authorization_nonce> methods as shown below.
440
441=back
442
443=head1 METHODS
444
445=over 4
446
513d8ab6 447=item new $config, $c, $realm
d99b7693 448
513d8ab6 449Simple constructor.
d99b7693 450
513d8ab6 451=item authenticate $c, $realm, \%auth_info
d99b7693 452
513d8ab6 453Tries to authenticate the user, and if that fails calls
454C<authorization_required_response> and detaches the current action call stack.
d99b7693 455
456Looks inside C<< $c->request->headers >> and processes the digest and basic
457(badly named) authorization header.
458
459This will only try the methods set in the configuration. First digest, then basic.
460
bf399285 461The %auth_info hash can contain a number of keys which control the authentication behaviour:
462
463=over
464
465=item realm
466
467Sets the HTTP authentication realm presented to the client. Note this does not alter the
468Catalyst::Authentication::Realm object used for the authentication.
469
470=item password_type
471
472The type of password returned by the user object. Same useage as in
473L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/passwprd_type>
474
475=item password_field
476
477The name of accessor used to retrieve the value of the password field from the user object. Same useage as in
478L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
479
480=back
d99b7693 481
513d8ab6 482=item authenticate_basic $c, $realm, \%auth_info
d99b7693 483
bf399285 484Performs HTTP basic authentication.
490754a8 485
513d8ab6 486=item authenticate_digest $c, $realm, \%auth_info
d99b7693 487
bf399285 488Performs HTTP digest authentication. Note that the password_type B<must> by I<clear> for
489digest authentication to succeed.
d99b7693 490
513d8ab6 491=item authorization_required_response $c, $realm, \%auth_info
d99b7693 492
493Sets C<< $c->response >> to the correct status code, and adds the correct
494header to demand authentication data from the user agent.
495
513d8ab6 496Typically used by C<authenticate>, but may be invoked manually.
d99b7693 497
513d8ab6 498%opts can contain C<domain> and C<algorithm>, which are used to build
d99b7693 499%the digest header.
500
513d8ab6 501=item store_digest_authorization_nonce $c, $key, $nonce
d99b7693 502
513d8ab6 503=item get_digest_authorization_nonce $c, $key
d99b7693 504
505Set or get the C<$nonce> object used by the digest auth mode.
506
507You may override these methods. By default they will call C<get> and C<set> on
508C<< $c->cache >>.
509
d99b7693 510=back
511
512=head1 CONFIGURATION
513
513d8ab6 514All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
d99b7693 515
516This should be a hash, and it can contain the following entries:
517
518=over 4
519
d99b7693 520=item type
521
522Can be either C<any> (the default), C<basic> or C<digest>.
523
513d8ab6 524This controls C<authorization_required_response> and C<authenticate>, but
d99b7693 525not the "manual" methods.
526
527=item authorization_required_message
528
513d8ab6 529Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
d99b7693 530
531=back
532
533=head1 RESTRICTIONS
534
535When using digest authentication, this module will only work together
536with authentication stores whose User objects have a C<password>
537method that returns the plain-text password. It will not work together
538with L<Catalyst::Authentication::Store::Htpasswd>, or
513d8ab6 539L<Catalyst::Authentication::Store::DBIC> stores whose
d99b7693 540C<password> methods return a hashed or salted version of the password.
c7b3e379 541
a14203f8 542=head1 AUTHORS
543
513d8ab6 544Updated to current name space and currently maintained
545by: Tomas Doran C<bobtfish@bobtfish.net>.
546
547Original module by:
548
549=over
a14203f8 550
513d8ab6 551=item Yuval Kogman, C<nothingmuch@woobling.org>
a14203f8 552
513d8ab6 553=item Jess Robinson
554
555=item Sascha Kiefer C<esskar@cpan.org>
556
557=back
a14203f8 558
c7b3e379 559=head1 SEE ALSO
560
d99b7693 561RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
c7b3e379 562
a14203f8 563=head1 COPYRIGHT & LICENSE
564
513d8ab6 565 Copyright (c) 2005-2008 the aforementioned authors. All rights
a14203f8 566 reserved. This program is free software; you can redistribute
a14203f8 567 it and/or modify it under the same terms as Perl itself.
568
a14203f8 569=cut
513d8ab6 570