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