Checking in changes prior to tagging of version 1.003. 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
f1f73b53 16our $VERSION = "1.003";
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 if ( my $domain = $opts->{domain} ) {
262 Catalyst::Exception->throw("domain must be an array reference")
263 unless ref($domain) && ref($domain) eq "ARRAY";
264
265 my @uris =
513d8ab6 266 $self->_config->{use_uri_for}
d99b7693 267 ? ( map { $c->uri_for($_) } @$domain )
268 : ( map { URI::Escape::uri_escape($_) } @$domain );
269
270 return qq{domain="@uris"};
513d8ab6 271 }
272 return;
d99b7693 273}
274
275sub _build_auth_header_common {
513d8ab6 276 my ( $self, $c, $opts ) = @_;
f1f73b53 277warn("HERE Opts $opts");
d99b7693 278 return (
bf399285 279 $self->_build_auth_header_realm($c, $opts),
513d8ab6 280 $self->_build_auth_header_domain($c, $opts),
d99b7693 281 );
282}
283
284sub _build_basic_auth_header {
513d8ab6 285 my ( $self, $c, $opts ) = @_;
286 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
d99b7693 287}
288
289sub _build_digest_auth_header {
513d8ab6 290 my ( $self, $c, $opts ) = @_;
d99b7693 291
513d8ab6 292 my $nonce = $self->_digest_auth_nonce($c, $opts);
d99b7693 293
294 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
295
513d8ab6 296 $self->store_digest_authorization_nonce( $c, $key, $nonce );
a14203f8 297
513d8ab6 298 return _join_auth_header_parts( Digest =>
299 $self->_build_auth_header_common($c, $opts),
d99b7693 300 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
301 qop
302 nonce
303 opaque
304 algorithm
305 ),
306 );
307}
a14203f8 308
d99b7693 309sub _digest_auth_nonce {
513d8ab6 310 my ( $self, $c, $opts ) = @_;
d99b7693 311
312 my $package = __PACKAGE__ . '::Nonce';
313
314 my $nonce = $package->new;
315
513d8ab6 316 if ( my $algorithm = $opts->{algorithm} || $self->_config->{algorithm}) {
d99b7693 317 $nonce->algorithm( $algorithm );
318 }
319
320 return $nonce;
321}
322
323sub _join_auth_header_parts {
513d8ab6 324 my ( $type, @parts ) = @_;
d99b7693 325 return "$type " . join(", ", @parts );
326}
327
328sub get_digest_authorization_nonce {
513d8ab6 329 my ( $self, $c, $key ) = @_;
330
331 _check_cache($c);
332 return $c->cache->get( $key );
d99b7693 333}
334
335sub store_digest_authorization_nonce {
513d8ab6 336 my ( $self, $c, $key, $nonce ) = @_;
d99b7693 337
513d8ab6 338 _check_cache($c);
339 return $c->cache->set( $key, $nonce );
d99b7693 340}
341
513d8ab6 342package Catalyst::Authentication::Credential::HTTP::Nonce;
d99b7693 343
344use strict;
345use base qw[ Class::Accessor::Fast ];
346use Data::UUID ();
347
513d8ab6 348our $VERSION = '0.02';
d99b7693 349
350__PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
351
352sub new {
353 my $class = shift;
354 my $self = $class->SUPER::new(@_);
355
356 $self->nonce( Data::UUID->new->create_b64 );
357 $self->opaque( Data::UUID->new->create_b64 );
358 $self->qop('auth,auth-int');
359 $self->nonce_count('0x0');
360 $self->algorithm('MD5');
361
362 return $self;
363}
a14203f8 364
a14203f8 3651;
366
a14203f8 367__END__
368
a14203f8 369=pod
370
a14203f8 371=head1 NAME
372
513d8ab6 373Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
53306b93 374for Catalyst.
a14203f8 375
a14203f8 376=head1 SYNOPSIS
377
a14203f8 378 use Catalyst qw/
a14203f8 379 Authentication
a14203f8 380 /;
381
513d8ab6 382 __PACKAGE__->config( authentication => {
383 realms => {
384 example => {
385 credential => {
386 class => 'HTTP',
387 type => 'any', # or 'digest' or 'basic'
490754a8 388 password_type => 'clear',
389 password_field => 'password'
513d8ab6 390 },
391 store => {
392 class => 'Minimal',
393 users => {
394 Mufasa => { password => "Circle Of Life", },
395 },
396 },
397 },
398 }
399 });
d99b7693 400
401 sub foo : Local {
402 my ( $self, $c ) = @_;
403
513d8ab6 404 $c->authenticate({ realm => "example" });
d99b7693 405 # either user gets authenticated or 401 is sent
bf399285 406 # Note that the authentication realm sent to the client is overridden
407 # here, but this does not affect the Catalyst::Authentication::Realm
408 # used for authentication.
d99b7693 409
410 do_stuff();
411 }
031f556c 412
413 sub always_auth : Local {
414 my ( $self, $c ) = @_;
415
416 # Force authorization headers onto the response so that the user
417 # is asked again for authentication, even if they successfully
418 # authenticated.
419 my $realm = $c->get_auth_realm('example');
420 $realm->credential->authorization_required_response($c, $realm);
421 }
d99b7693 422
423 # with ACL plugin
513d8ab6 424 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
d99b7693 425
a14203f8 426=head1 DESCRIPTION
427
513d8ab6 428This module lets you use HTTP authentication with
d99b7693 429L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
430are currently supported.
431
432When authentication is required, this module sets a status of 401, and
433the body of the response to 'Authorization required.'. To override
434this and set your own content, check for the C<< $c->res->status ==
435401 >> in your C<end> action, and change the body accordingly.
436
437=head2 TERMS
438
439=over 4
440
441=item Nonce
442
443A nonce is a one-time value sent with each digest authentication
444request header. The value must always be unique, so per default the
445last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
446change this behaviour, override the
447C<store_digest_authorization_nonce> and
448C<get_digest_authorization_nonce> methods as shown below.
449
450=back
451
452=head1 METHODS
453
454=over 4
455
513d8ab6 456=item new $config, $c, $realm
d99b7693 457
513d8ab6 458Simple constructor.
d99b7693 459
513d8ab6 460=item authenticate $c, $realm, \%auth_info
d99b7693 461
513d8ab6 462Tries to authenticate the user, and if that fails calls
463C<authorization_required_response> and detaches the current action call stack.
d99b7693 464
465Looks inside C<< $c->request->headers >> and processes the digest and basic
466(badly named) authorization header.
467
468This will only try the methods set in the configuration. First digest, then basic.
469
bf399285 470The %auth_info hash can contain a number of keys which control the authentication behaviour:
471
472=over
473
474=item realm
475
476Sets the HTTP authentication realm presented to the client. Note this does not alter the
477Catalyst::Authentication::Realm object used for the authentication.
478
05512a69 479=item domain
bf399285 480
05512a69 481Array reference to domains used to build the authorization headers.
bf399285 482
483=back
d99b7693 484
513d8ab6 485=item authenticate_basic $c, $realm, \%auth_info
d99b7693 486
bf399285 487Performs HTTP basic authentication.
490754a8 488
513d8ab6 489=item authenticate_digest $c, $realm, \%auth_info
d99b7693 490
bf399285 491Performs HTTP digest authentication. Note that the password_type B<must> by I<clear> for
492digest authentication to succeed.
d99b7693 493
513d8ab6 494=item authorization_required_response $c, $realm, \%auth_info
d99b7693 495
496Sets C<< $c->response >> to the correct status code, and adds the correct
497header to demand authentication data from the user agent.
498
513d8ab6 499Typically used by C<authenticate>, but may be invoked manually.
d99b7693 500
513d8ab6 501%opts can contain C<domain> and C<algorithm>, which are used to build
d99b7693 502%the digest header.
503
513d8ab6 504=item store_digest_authorization_nonce $c, $key, $nonce
d99b7693 505
513d8ab6 506=item get_digest_authorization_nonce $c, $key
d99b7693 507
508Set or get the C<$nonce> object used by the digest auth mode.
509
510You may override these methods. By default they will call C<get> and C<set> on
511C<< $c->cache >>.
512
d99b7693 513=back
514
515=head1 CONFIGURATION
516
513d8ab6 517All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
d99b7693 518
519This should be a hash, and it can contain the following entries:
520
05512a69 521=over
d99b7693 522
d99b7693 523=item type
524
525Can be either C<any> (the default), C<basic> or C<digest>.
526
513d8ab6 527This controls C<authorization_required_response> and C<authenticate>, but
d99b7693 528not the "manual" methods.
529
530=item authorization_required_message
531
513d8ab6 532Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
d99b7693 533
05512a69 534=item password_type
535
f1f73b53 536The type of password returned by the user object. Same usage as in
05512a69 537L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/passwprd_type>
538
539=item password_field
540
f1f73b53 541The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
05512a69 542L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
543
544=item use_uri_for
545
546If this configuration key has a true value, then the domain(s) for the authorization header will be
547run through $c->uri_for()
548
d99b7693 549=back
550
551=head1 RESTRICTIONS
552
553When using digest authentication, this module will only work together
554with authentication stores whose User objects have a C<password>
555method that returns the plain-text password. It will not work together
556with L<Catalyst::Authentication::Store::Htpasswd>, or
513d8ab6 557L<Catalyst::Authentication::Store::DBIC> stores whose
d99b7693 558C<password> methods return a hashed or salted version of the password.
c7b3e379 559
a14203f8 560=head1 AUTHORS
561
513d8ab6 562Updated to current name space and currently maintained
563by: Tomas Doran C<bobtfish@bobtfish.net>.
564
565Original module by:
566
567=over
a14203f8 568
513d8ab6 569=item Yuval Kogman, C<nothingmuch@woobling.org>
a14203f8 570
513d8ab6 571=item Jess Robinson
572
573=item Sascha Kiefer C<esskar@cpan.org>
574
575=back
a14203f8 576
c7b3e379 577=head1 SEE ALSO
578
d99b7693 579RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
c7b3e379 580
a14203f8 581=head1 COPYRIGHT & LICENSE
582
513d8ab6 583 Copyright (c) 2005-2008 the aforementioned authors. All rights
a14203f8 584 reserved. This program is free software; you can redistribute
a14203f8 585 it and/or modify it under the same terms as Perl itself.
586
a14203f8 587=cut
513d8ab6 588