Add a body to the 401 status, add more docs!
[catagits/Catalyst-Authentication-Credential-HTTP.git] / lib / Catalyst / Plugin / Authentication / Credential / HTTP.pm
CommitLineData
a14203f8 1#!/usr/bin/perl
2
a14203f8 3package Catalyst::Plugin::Authentication::Credential::HTTP;
a14203f8 4use base qw/Catalyst::Plugin::Authentication::Credential::Password/;
5
a14203f8 6use strict;
a14203f8 7use warnings;
8
a14203f8 9use String::Escape ();
a14203f8 10use URI::Escape ();
a14203f8 11use Catalyst ();
a14203f8 12use Digest::MD5 ();
13
2d4d6aac 14our $VERSION = "0.08";
a14203f8 15
a14203f8 16sub authenticate_http {
ac92fd52 17 my ( $c, @args ) = @_;
a14203f8 18
ac92fd52 19 return 1 if $c->_is_http_auth_type('digest') && $c->authenticate_digest(@args);
20 return 1 if $c->_is_http_auth_type('basic') && $c->authenticate_basic(@args);
a14203f8 21}
22
ac92fd52 23sub get_http_auth_store {
24 my ( $c, %opts ) = @_;
371a8cc8 25
26 my $store = $opts{store} || $c->config->{authentication}{http}{store} || return;
27
28 return ref $store
29 ? $store
30 : $c->get_auth_store($store);
ac92fd52 31}
a14203f8 32
33sub authenticate_basic {
ac92fd52 34 my ( $c, %opts ) = @_;
a14203f8 35
36 $c->log->debug('Checking http basic authentication.') if $c->debug;
37
a14203f8 38 my $headers = $c->req->headers;
39
ac92fd52 40 if ( my ( $username, $password ) = $headers->authorization_basic ) {
a14203f8 41
ac92fd52 42 my $user;
a14203f8 43
ac92fd52 44 unless ( $user = $opts{user} ) {
45 if ( my $store = $c->get_http_auth_store(%opts) ) {
46 $user = $store->get_user($username);
47 } else {
48 $user = $username;
49 }
a14203f8 50 }
51
a14203f8 52 return $c->login( $user, $password );
a14203f8 53 }
54
a14203f8 55 return 0;
a14203f8 56}
57
a14203f8 58sub authenticate_digest {
ac92fd52 59 my ( $c, %opts ) = @_;
a14203f8 60
61 $c->log->debug('Checking http digest authentication.') if $c->debug;
62
a14203f8 63 my $headers = $c->req->headers;
a14203f8 64 my @authorization = $headers->header('Authorization');
a14203f8 65 foreach my $authorization (@authorization) {
a14203f8 66 next unless $authorization =~ m{^Digest};
67
a14203f8 68 my %res = map {
a14203f8 69 my @key_val = split /=/, $_, 2;
a14203f8 70 $key_val[0] = lc $key_val[0];
a14203f8 71 $key_val[1] =~ s{"}{}g; # remove the quotes
a14203f8 72 @key_val;
a14203f8 73 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
74
a14203f8 75 my $opaque = $res{opaque};
371a8cc8 76 my $nonce = $c->get_digest_authorization_nonce( __PACKAGE__ . '::opaque:' . $opaque );
a14203f8 77 next unless $nonce;
78
a14203f8 79 $c->log->debug('Checking authentication parameters.')
a14203f8 80 if $c->debug;
81
a14203f8 82 my $uri = '/' . $c->request->path;
a14203f8 83 my $algorithm = $res{algorithm} || 'MD5';
a14203f8 84 my $nonce_count = '0x' . $res{nc};
85
a14203f8 86 my $check = $uri eq $res{uri}
a14203f8 87 && ( exists $res{username} )
a14203f8 88 && ( exists $res{qop} )
a14203f8 89 && ( exists $res{cnonce} )
a14203f8 90 && ( exists $res{nc} )
a14203f8 91 && $algorithm eq $nonce->algorithm
a14203f8 92 && hex($nonce_count) > hex( $nonce->nonce_count )
a14203f8 93 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
94
a14203f8 95 unless ($check) {
a14203f8 96 $c->log->debug('Digest authentication failed. Bad request.')
a14203f8 97 if $c->debug;
a14203f8 98 $c->res->status(400); # bad request
a14203f8 99 die $Catalyst::DETACH;
a14203f8 100 }
101
a14203f8 102 $c->log->debug('Checking authentication response.')
a14203f8 103 if $c->debug;
104
a14203f8 105 my $username = $res{username};
a14203f8 106 my $realm = $res{realm};
107
a14203f8 108 my $user;
a14203f8 109
371a8cc8 110 unless ( $user = $opts{user} ) {
111 if ( my $store = $c->get_http_auth_store(%opts) || $c->default_auth_store ) {
112 $user = $store->get_user($username);
113 }
114 }
a14203f8 115
116 unless ($user) { # no user, no authentication
a14203f8 117 $c->log->debug('Unknown user: $user.') if $c->debug;
a14203f8 118 return 0;
a14203f8 119 }
120
a14203f8 121 # everything looks good, let's check the response
122
a14203f8 123 # calculate H(A2) as per spec
a14203f8 124 my $ctx = Digest::MD5->new;
a14203f8 125 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
a14203f8 126 if ( $res{qop} eq 'auth-int' ) {
a14203f8 127 my $digest =
a14203f8 128 Digest::MD5::md5_hex( $c->request->body ); # not sure here
a14203f8 129 $ctx->add( ':', $digest );
a14203f8 130 }
a14203f8 131 my $A2_digest = $ctx->hexdigest;
132
a14203f8 133 # the idea of the for loop:
a14203f8 134 # if we do not want to store the plain password in our user store,
a14203f8 135 # we can store md5_hex("$username:$realm:$password") instead
a14203f8 136 for my $r ( 0 .. 1 ) {
137
a14203f8 138 # calculate H(A1) as per spec
a14203f8 139 my $A1_digest = $r ? $user->password : do {
a14203f8 140 $ctx = Digest::MD5->new;
a14203f8 141 $ctx->add( join( ':', $username, $realm, $user->password ) );
a14203f8 142 $ctx->hexdigest;
a14203f8 143 };
a14203f8 144 if ( $nonce->algorithm eq 'MD5-sess' ) {
a14203f8 145 $ctx = Digest::MD5->new;
a14203f8 146 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
a14203f8 147 $A1_digest = $ctx->hexdigest;
a14203f8 148 }
149
a14203f8 150 my $rq_digest = Digest::MD5::md5_hex(
a14203f8 151 join( ':',
a14203f8 152 $A1_digest, $res{nonce},
a14203f8 153 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
a14203f8 154 $A2_digest )
a14203f8 155 );
156
a14203f8 157 $nonce->nonce_count($nonce_count);
a14203f8 158 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
a14203f8 159 $nonce );
160
a14203f8 161 return $c->login( $user, $user->password )
a14203f8 162 if $rq_digest eq $res{response};
a14203f8 163 }
a14203f8 164 }
165
a14203f8 166 return 0;
a14203f8 167}
168
a14203f8 169sub _check_cache {
a14203f8 170 my $c = shift;
171
a14203f8 172 die "A cache is needed for http digest authentication."
a14203f8 173 unless $c->can('cache');
a14203f8 174}
175
ac92fd52 176sub _is_http_auth_type {
a14203f8 177 my ( $c, $type ) = @_;
178
a14203f8 179 my $cfgtype = lc( $c->config->{authentication}{http}{type} || 'any' );
a14203f8 180 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
a14203f8 181 return 0;
a14203f8 182}
183
a14203f8 184sub authorization_required {
ac92fd52 185 my ( $c, @args ) = @_;
a14203f8 186
ac92fd52 187 return 1 if $c->authenticate_http(@args);
188
189 $c->authorization_required_response(@args);
a14203f8 190
191 die $Catalyst::DETACH;
a14203f8 192}
193
a14203f8 194sub authorization_required_response {
a14203f8 195 my ( $c, %opts ) = @_;
196
a14203f8 197 $c->res->status(401);
c7b3e379 198 $c->res->content_type('text/plain');
199 $c->res->body($c->config->{authentication}{http}{authorization_required_message} ||
200 $opts{authorization_required_message} ||
201 'Authorization required.');
a14203f8 202
ac92fd52 203 # *DONT* short circuit
204 my $ok;
205 $ok++ if $c->_create_digest_auth_response(\%opts);
206 $ok++ if $c->_create_basic_auth_response(\%opts);
a14203f8 207
ac92fd52 208 unless ( $ok ) {
209 die 'Could not build authorization required response. '
210 . 'Did you configure a valid authentication http type: '
211 . 'basic, digest, any';
212 }
a14203f8 213}
214
ac92fd52 215sub _add_authentication_header {
216 my ( $c, $header ) = @_;
217 $c->res->headers->push_header( 'WWW-Authenticate' => $header );
218}
a14203f8 219
ac92fd52 220sub _create_digest_auth_response {
221 my ( $c, $opts ) = @_;
222
223 return unless $c->_is_http_auth_type('digest');
224
225 if ( my $digest = $c->_build_digest_auth_header( $opts ) ) {
226 $c->_add_authentication_header( $digest );
227 return 1;
228 }
a14203f8 229
ac92fd52 230 return;
231}
a14203f8 232
ac92fd52 233sub _create_basic_auth_response {
234 my ( $c, $opts ) = @_;
235
236 return unless $c->_is_http_auth_type('basic');
a14203f8 237
ac92fd52 238 if ( my $basic = $c->_build_basic_auth_header( $opts ) ) {
239 $c->_add_authentication_header( $basic );
240 return 1;
241 }
a14203f8 242
ac92fd52 243 return;
244}
a14203f8 245
ac92fd52 246sub _build_auth_header_realm {
247 my ( $c, $opts ) = @_;
a14203f8 248
249 if ( my $realm = $opts->{realm} ) {
ac92fd52 250 return 'realm=' . String::Escape::qprintable($realm);
251 } else {
252 return;
a14203f8 253 }
ac92fd52 254}
a14203f8 255
ac92fd52 256sub _build_auth_header_domain {
257 my ( $c, $opts ) = @_;
a14203f8 258
259 if ( my $domain = $opts->{domain} ) {
18eacbdd 260 Catalyst::Exception->throw("domain must be an array reference")
a14203f8 261 unless ref($domain) && ref($domain) eq "ARRAY";
262
a14203f8 263 my @uris =
a14203f8 264 $c->config->{authentication}{http}{use_uri_for}
a14203f8 265 ? ( map { $c->uri_for($_) } @$domain )
a14203f8 266 : ( map { URI::Escape::uri_escape($_) } @$domain );
267
ac92fd52 268 return qq{domain="@uris"};
269 } else {
270 return;
a14203f8 271 }
ac92fd52 272}
a14203f8 273
ac92fd52 274sub _build_auth_header_common {
275 my ( $c, $opts ) = @_;
a14203f8 276
ac92fd52 277 return (
278 $c->_build_auth_header_realm($opts),
279 $c->_build_auth_header_domain($opts),
280 );
281}
a14203f8 282
ac92fd52 283sub _build_basic_auth_header {
284 my ( $c, $opts ) = @_;
18eacbdd 285 return $c->_join_auth_header_parts( Basic => $c->_build_auth_header_common( $opts ) );
ac92fd52 286}
a14203f8 287
ac92fd52 288sub _build_digest_auth_header {
289 my ( $c, $opts ) = @_;
a14203f8 290
ac92fd52 291 my $nonce = $c->_digest_auth_nonce($opts);
a14203f8 292
ac92fd52 293 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
294
371a8cc8 295 $c->store_digest_authorization_nonce( $key, $nonce );
a14203f8 296
ac92fd52 297 return $c->_join_auth_header_parts( Digest =>
298 $c->_build_auth_header_common($opts),
299 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
300 qop
301 nonce
302 opaque
303 algorithm
304 ),
305 );
306}
a14203f8 307
ac92fd52 308sub _digest_auth_nonce {
309 my ( $c, $opts ) = @_;
a14203f8 310
ac92fd52 311 my $package = __PACKAGE__ . '::Nonce';
a14203f8 312
ac92fd52 313 my $nonce = $package->new;
a14203f8 314
371a8cc8 315 if ( my $algorithm = $opts->{algorithm} || $c->config->{authentication}{http}{algorithm}) {
316 $nonce->algorithm( $algorithm );
317 }
a14203f8 318
ac92fd52 319 return $nonce;
320}
a14203f8 321
ac92fd52 322sub _join_auth_header_parts {
323 my ( $c, $type, @parts ) = @_;
324 return "$type " . join(", ", @parts );
325}
a14203f8 326
371a8cc8 327sub get_digest_authorization_nonce {
ac92fd52 328 my ( $c, $key ) = @_;
a14203f8 329
ac92fd52 330 $c->_check_cache;
331 $c->cache->get( $key );
a14203f8 332}
333
371a8cc8 334sub store_digest_authorization_nonce {
ac92fd52 335 my ( $c, $key, $nonce ) = @_;
a14203f8 336
ac92fd52 337 $c->_check_cache;
338 $c->cache->set( $key, $nonce );
339}
a14203f8 340
341package Catalyst::Plugin::Authentication::Credential::HTTP::Nonce;
342
a14203f8 343use strict;
a14203f8 344use base qw[ Class::Accessor::Fast ];
a14203f8 345use Data::UUID ();
346
a14203f8 347our $VERSION = "0.01";
348
a14203f8 349__PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
350
a14203f8 351sub new {
a14203f8 352 my $class = shift;
a14203f8 353 my $self = $class->SUPER::new(@_);
354
a14203f8 355 $self->nonce( Data::UUID->new->create_b64 );
a14203f8 356 $self->opaque( Data::UUID->new->create_b64 );
a14203f8 357 $self->qop('auth,auth-int');
a14203f8 358 $self->nonce_count('0x0');
a14203f8 359 $self->algorithm('MD5');
360
a14203f8 361 return $self;
a14203f8 362}
363
a14203f8 3641;
365
a14203f8 366__END__
367
a14203f8 368=pod
369
a14203f8 370=head1 NAME
371
a14203f8 372Catalyst::Plugin::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
53306b93 373for Catalyst.
a14203f8 374
a14203f8 375=head1 SYNOPSIS
376
a14203f8 377 use Catalyst qw/
a14203f8 378 Authentication
c7b3e379 379 Authentication::Store::Minimal
a14203f8 380 Authentication::Credential::HTTP
a14203f8 381 /;
382
3bb378d2 383 __PACKAGE__->config->{authentication}{http}{type} = 'any'; # or 'digest' or 'basic'
384 __PACKAGE__->config->{authentication}{users} = {
385 Mufasa => { password => "Circle Of Life", },
a14203f8 386 };
387
a14203f8 388 sub foo : Local {
a14203f8 389 my ( $self, $c ) = @_;
390
a14203f8 391 $c->authorization_required( realm => "foo" ); # named after the status code ;-)
392
a14203f8 393 # either user gets authenticated or 401 is sent
394
a14203f8 395 do_stuff();
a14203f8 396 }
397
a14203f8 398 # with ACL plugin
a14203f8 399 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate_http });
400
a14203f8 401 sub end : Private {
a14203f8 402 my ( $self, $c ) = @_;
403
a14203f8 404 $c->authorization_required_response( realm => "foo" );
a14203f8 405 $c->error(0);
a14203f8 406 }
407
a14203f8 408=head1 DESCRIPTION
409
a14203f8 410This moduule lets you use HTTP authentication with
a14203f8 411L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
a14203f8 412are currently supported.
413
c7b3e379 414When authentication is required, this module sets a status of 401, and
415the body of the response to 'Authorization required.'. To override
416this and set your own content, check for the C<< $c->res->status ==
417401 >> in your C<end> action, and change the body accordingly.
418
419=head2 TERMS
420
421=over 4
422
423=item Nonce
424
425A nonce is a one-time value sent with each digest authentication
426request header. The value must always be unique, so per default the
427last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
428change this behaviour, override the
429C<store_digest_authorization_nonce> and
430C<get_digest_authorization_nonce> methods as shown below.
431
432=back
433
a14203f8 434=head1 METHODS
435
a14203f8 436=over 4
437
371a8cc8 438=item authorization_required %opts
a14203f8 439
a14203f8 440Tries to C<authenticate_http>, and if that fails calls
a14203f8 441C<authorization_required_response> and detaches the current action call stack.
442
371a8cc8 443This method just passes the options through untouched.
444
445=item authenticate_http %opts
a14203f8 446
a14203f8 447Looks inside C<< $c->request->headers >> and processes the digest and basic
a14203f8 448(badly named) authorization header.
449
c7b3e379 450This will only try the methods set in the configuration. First digest, then basic.
371a8cc8 451
452See the next two methods for what %opts can contain.
453
454=item authenticate_basic %opts
455
456=item authenticate_digest %opts
457
458Try to authenticate one of the methods without checking if the method is
459allowed in the configuration.
460
461%opts can contain C<store> (either an object or a name), C<user> (to disregard
462%the username from the header altogether, overriding it with a username or user
463%object).
464
465=item authorization_required_response %opts
a14203f8 466
a14203f8 467Sets C<< $c->response >> to the correct status code, and adds the correct
a14203f8 468header to demand authentication data from the user agent.
469
371a8cc8 470Typically used by C<authorization_required>, but may be invoked manually.
471
472%opts can contain C<realm>, C<domain> and C<algorithm>, which are used to build
473%the digest header.
474
475=item store_digest_authorization_nonce $key, $nonce
476
477=item get_digest_authorization_nonce $key
478
479Set or get the C<$nonce> object used by the digest auth mode.
480
481You may override these methods. By default they will call C<get> and C<set> on
482C<< $c->cache >>.
483
484=back
485
486=head1 CONFIGURATION
487
488All configuration is stored in C<< YourApp->config->{authentication}{http} >>.
489
490This should be a hash, and it can contain the following entries:
491
492=over 4
493
494=item store
495
496Either a name or an object -- the default store to use for HTTP authentication.
497
498=item type
499
500Can be either C<any> (the default), C<basic> or C<digest>.
501
502This controls C<authorization_required_response> and C<authenticate_http>, but
503not the "manual" methods.
504
c7b3e379 505=item authorization_required_message
506
507Set this to a string to override the default body content "Authorization required."
508
a14203f8 509=back
510
c7b3e379 511=head1 RESTRICTIONS
512
513When using digest authentication, this module will only work together
514with authentication stores whose User objects have a C<password>
515method that returns the plain-text password. It will not work together
516with L<Catalyst::Authentication::Store::Htpasswd>, or
517L<Catalyst::Plugin::Authentication::Store::DBIC> stores whose
518C<password> methods return a hashed or salted version of the password.
519
a14203f8 520=head1 AUTHORS
521
a14203f8 522Yuval Kogman, C<nothingmuch@woobling.org>
523
a14203f8 524Jess Robinson
525
a14203f8 526Sascha Kiefer C<esskar@cpan.org>
527
c7b3e379 528=head1 SEE ALSO
529
530RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
531
a14203f8 532=head1 COPYRIGHT & LICENSE
533
a14203f8 534 Copyright (c) 2005-2006 the aforementioned authors. All rights
a14203f8 535 reserved. This program is free software; you can redistribute
a14203f8 536 it and/or modify it under the same terms as Perl itself.
537
a14203f8 538=cut