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