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