X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FCatalyst%2FAuthentication%2FCredential%2FHTTP.pm;h=27a59ab638f0d217d4080498116494cf2286c8e4;hb=c5a1fa888d417417060fa21bd72f5d862cb60513;hp=aa043260c26c275a67ab86e32ec624f24dff4fa9;hpb=eebdb0e48f5073a81e9a055e80457a8a2480e7bc;p=catagits%2FCatalyst-Authentication-Credential-HTTP.git diff --git a/lib/Catalyst/Authentication/Credential/HTTP.pm b/lib/Catalyst/Authentication/Credential/HTTP.pm index aa04326..27a59ab 100644 --- a/lib/Catalyst/Authentication/Credential/HTTP.pm +++ b/lib/Catalyst/Authentication/Credential/HTTP.pm @@ -9,53 +9,68 @@ use URI::Escape (); use Catalyst (); use Digest::MD5 (); -our $VERSION = "0.11"; +BEGIN { + __PACKAGE__->mk_accessors(qw/_config realm/); +} -sub authenticate_http { - my ( $c, @args ) = @_; +our $VERSION = "1.004"; - return 1 if $c->_is_http_auth_type('digest') && $c->authenticate_digest(@args); - return 1 if $c->_is_http_auth_type('basic') && $c->authenticate_basic(@args); - return; +sub new { + my ($class, $config, $app, $realm) = @_; + + my $self = { _config => $config, _debug => $app->debug }; + bless $self, $class; + + $self->realm($realm); + + my $type = $self->_config->{'type'} ||= 'any'; + + if (!grep /$type/, ('basic', 'digest', 'any')) { + Catalyst::Exception->throw(__PACKAGE__ . " used with unsupported authentication type: " . $type); + } + return $self; } -sub get_http_auth_store { - my ( $c, %opts ) = @_; +sub authenticate { + my ( $self, $c, $realm, $auth_info ) = @_; + my $auth; - my $store = $opts{store} || $c->config->{authentication}{http}{store} || return; + $auth = $self->authenticate_digest($c, $realm, $auth_info) if $self->_is_http_auth_type('digest'); + return $auth if $auth; - return ref $store - ? $store - : $c->get_auth_store($store); + $auth = $self->authenticate_basic($c, $realm, $auth_info) if $self->_is_http_auth_type('basic'); + return $auth if $auth; + + $self->authorization_required_response($c, $realm, $auth_info); + die $Catalyst::DETACH; } sub authenticate_basic { - my ( $c, %opts ) = @_; + my ( $self, $c, $realm, $auth_info ) = @_; $c->log->debug('Checking http basic authentication.') if $c->debug; my $headers = $c->req->headers; if ( my ( $username, $password ) = $headers->authorization_basic ) { - - my $user; - - unless ( $user = $opts{user} ) { - if ( my $store = $c->get_http_auth_store(%opts) ) { - $user = $store->get_user($username); - } else { - $user = $username; + my $user_obj = $realm->find_user( { username => $username }, $c); + if (ref($user_obj)) { + if ($self->check_password($user_obj, {$self->_config->{password_field} => $password})) { + $c->set_authenticated($user_obj); + return $user_obj; } } - - return $c->login( $user, $password ); + else { + $c->log->debug("Unable to locate user matching user info provided") if $c->debug; + return; + } } - return 0; + return; } sub authenticate_digest { - my ( $c, %opts ) = @_; + my ( $self, $c, $realm, $auth_info ) = @_; $c->log->debug('Checking http digest authentication.') if $c->debug; @@ -63,7 +78,6 @@ sub authenticate_digest { my @authorization = $headers->header('Authorization'); foreach my $authorization (@authorization) { next unless $authorization =~ m{^Digest}; - my %res = map { my @key_val = split /=/, $_, 2; $key_val[0] = lc $key_val[0]; @@ -72,7 +86,7 @@ sub authenticate_digest { } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest " my $opaque = $res{opaque}; - my $nonce = $c->get_digest_authorization_nonce( __PACKAGE__ . '::opaque:' . $opaque ); + my $nonce = $self->get_digest_authorization_nonce( $c, __PACKAGE__ . '::opaque:' . $opaque ); next unless $nonce; $c->log->debug('Checking authentication parameters.') @@ -95,30 +109,25 @@ sub authenticate_digest { $c->log->debug('Digest authentication failed. Bad request.') if $c->debug; $c->res->status(400); # bad request - die $Catalyst::DETACH; + Carp::confess $Catalyst::DETACH; } $c->log->debug('Checking authentication response.') if $c->debug; my $username = $res{username}; - my $realm = $res{realm}; my $user; - unless ( $user = $opts{user} ) { - if ( my $store = $c->get_http_auth_store(%opts) || $c->default_auth_store ) { - $user = $store->get_user($username); - } + unless ( $user = $auth_info->{user} ) { + $user = $realm->find_user( { username => $username }, $c); } - unless ($user) { # no user, no authentication - $c->log->debug('Unknown user: $user.') if $c->debug; - return 0; + $c->log->debug("Unable to locate user matching user info provided") if $c->debug; + return; } # everything looks good, let's check the response - # calculate H(A2) as per spec my $ctx = Digest::MD5->new; $ctx->add( join( ':', $c->request->method, $res{uri} ) ); @@ -132,12 +141,12 @@ sub authenticate_digest { # the idea of the for loop: # if we do not want to store the plain password in our user store, # we can store md5_hex("$username:$realm:$password") instead + my $password_field = $self->_config->{password_field}; for my $r ( 0 .. 1 ) { - # calculate H(A1) as per spec - my $A1_digest = $r ? $user->password : do { + my $A1_digest = $r ? $user->$password_field() : do { $ctx = Digest::MD5->new; - $ctx->add( join( ':', $username, $realm, $user->password ) ); + $ctx->add( join( ':', $username, $realm->name, $user->$password_field() ) ); $ctx->hexdigest; }; if ( $nonce->algorithm eq 'MD5-sess' ) { @@ -146,23 +155,21 @@ sub authenticate_digest { $A1_digest = $ctx->hexdigest; } - my $rq_digest = Digest::MD5::md5_hex( - join( ':', + my $digest_in = join( ':', $A1_digest, $res{nonce}, $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (), - $A2_digest ) - ); - + $A2_digest ); + my $rq_digest = Digest::MD5::md5_hex($digest_in); $nonce->nonce_count($nonce_count); $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque, $nonce ); - - return $c->login( $user, $user->password ) - if $rq_digest eq $res{response}; + if ($rq_digest eq $res{response}) { + $c->set_authenticated($user); + return 1; + } } } - - return 0; + return; } sub _check_cache { @@ -174,36 +181,30 @@ sub _check_cache { } sub _is_http_auth_type { - my ( $c, $type ) = @_; - - my $cfgtype = lc( $c->config->{authentication}{http}{type} || 'any' ); + my ( $self, $type ) = @_; + my $cfgtype = lc( $self->_config->{'type'} || 'any' ); return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type; return 0; } -sub authorization_required { - my ( $c, @args ) = @_; - - return 1 if $c->authenticate_http(@args); - - $c->authorization_required_response(@args); - - die $Catalyst::DETACH; -} - sub authorization_required_response { - my ( $c, %opts ) = @_; + my ( $self, $c, $realm, $auth_info ) = @_; $c->res->status(401); $c->res->content_type('text/plain'); - $c->res->body($c->config->{authentication}{http}{authorization_required_message} || - $opts{authorization_required_message} || - 'Authorization required.'); + if (exists $self->_config->{authorization_required_message}) { + # If you set the key to undef, don't stamp on the body. + $c->res->body($self->_config->{authorization_required_message}) + if defined $c->res->body($self->_config->{authorization_required_message}); + } + else { + $c->res->body('Authorization required.'); + } # *DONT* short circuit my $ok; - $ok++ if $c->_create_digest_auth_response(\%opts); - $ok++ if $c->_create_basic_auth_response(\%opts); + $ok++ if $self->_create_digest_auth_response($c, $auth_info); + $ok++ if $self->_create_basic_auth_response($c, $auth_info); unless ( $ok ) { die 'Could not build authorization required response. ' @@ -215,17 +216,17 @@ sub authorization_required_response { sub _add_authentication_header { my ( $c, $header ) = @_; - $c->res->headers->push_header( 'WWW-Authenticate' => $header ); + $c->response->headers->push_header( 'WWW-Authenticate' => $header ); return; } sub _create_digest_auth_response { - my ( $c, $opts ) = @_; + my ( $self, $c, $opts ) = @_; - return unless $c->_is_http_auth_type('digest'); + return unless $self->_is_http_auth_type('digest'); - if ( my $digest = $c->_build_digest_auth_header( $opts ) ) { - $c->_add_authentication_header( $digest ); + if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) { + _add_authentication_header( $c, $digest ); return 1; } @@ -233,12 +234,12 @@ sub _create_digest_auth_response { } sub _create_basic_auth_response { - my ( $c, $opts ) = @_; + my ( $self, $c, $opts ) = @_; - return unless $c->_is_http_auth_type('basic'); + return unless $self->_is_http_auth_type('basic'); - if ( my $basic = $c->_build_basic_auth_header( $opts ) ) { - $c->_add_authentication_header( $basic ); + if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) { + _add_authentication_header( $c, $basic ); return 1; } @@ -246,23 +247,22 @@ sub _create_basic_auth_response { } sub _build_auth_header_realm { - my ( $c, $opts ) = @_; - - if ( my $realm = $opts->{realm} ) { - return 'realm=' . String::Escape::qprintable($realm); + my ( $self, $c, $opts ) = @_; + if ( my $realm_name = String::Escape::qprintable($opts->{realm} ? $opts->{realm} : $self->realm->name) ) { + $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/; + return 'realm=' . $realm_name; } return; } sub _build_auth_header_domain { - my ( $c, $opts ) = @_; - + my ( $self, $c, $opts ) = @_; if ( my $domain = $opts->{domain} ) { Catalyst::Exception->throw("domain must be an array reference") unless ref($domain) && ref($domain) eq "ARRAY"; my @uris = - $c->config->{authentication}{http}{use_uri_for} + $self->_config->{use_uri_for} ? ( map { $c->uri_for($_) } @$domain ) : ( map { URI::Escape::uri_escape($_) } @$domain ); @@ -272,30 +272,29 @@ sub _build_auth_header_domain { } sub _build_auth_header_common { - my ( $c, $opts ) = @_; - + my ( $self, $c, $opts ) = @_; return ( - $c->_build_auth_header_realm($opts), - $c->_build_auth_header_domain($opts), + $self->_build_auth_header_realm($c, $opts), + $self->_build_auth_header_domain($c, $opts), ); } sub _build_basic_auth_header { - my ( $c, $opts ) = @_; - return $c->_join_auth_header_parts( Basic => $c->_build_auth_header_common( $opts ) ); + my ( $self, $c, $opts ) = @_; + return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) ); } sub _build_digest_auth_header { - my ( $c, $opts ) = @_; + my ( $self, $c, $opts ) = @_; - my $nonce = $c->_digest_auth_nonce($opts); + my $nonce = $self->_digest_auth_nonce($c, $opts); my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque; - $c->store_digest_authorization_nonce( $key, $nonce ); + $self->store_digest_authorization_nonce( $c, $key, $nonce ); - return $c->_join_auth_header_parts( Digest => - $c->_build_auth_header_common($opts), + return _join_auth_header_parts( Digest => + $self->_build_auth_header_common($c, $opts), map { sprintf '%s="%s"', $_, $nonce->$_ } qw( qop nonce @@ -306,13 +305,13 @@ sub _build_digest_auth_header { } sub _digest_auth_nonce { - my ( $c, $opts ) = @_; + my ( $self, $c, $opts ) = @_; my $package = __PACKAGE__ . '::Nonce'; my $nonce = $package->new; - if ( my $algorithm = $opts->{algorithm} || $c->config->{authentication}{http}{algorithm}) { + if ( my $algorithm = $opts->{algorithm} || $self->_config->{algorithm}) { $nonce->algorithm( $algorithm ); } @@ -320,21 +319,21 @@ sub _digest_auth_nonce { } sub _join_auth_header_parts { - my ( $c, $type, @parts ) = @_; + my ( $type, @parts ) = @_; return "$type " . join(", ", @parts ); } sub get_digest_authorization_nonce { - my ( $c, $key ) = @_; - - $c->_check_cache; + my ( $self, $c, $key ) = @_; + + _check_cache($c); return $c->cache->get( $key ); } sub store_digest_authorization_nonce { - my ( $c, $key, $nonce ) = @_; + my ( $self, $c, $key, $nonce ) = @_; - $c->_check_cache; + _check_cache($c); return $c->cache->set( $key, $nonce ); } @@ -376,35 +375,52 @@ for Catalyst. use Catalyst qw/ Authentication - Authentication::Store::Minimal - Authentication::Credential::HTTP /; - __PACKAGE__->config->{authentication}{http}{type} = 'any'; # or 'digest' or 'basic' - __PACKAGE__->config->{authentication}{users} = { - Mufasa => { password => "Circle Of Life", }, - }; + __PACKAGE__->config( authentication => { + realms => { + example => { + credential => { + class => 'HTTP', + type => 'any', # or 'digest' or 'basic' + password_type => 'clear', + password_field => 'password' + }, + store => { + class => 'Minimal', + users => { + Mufasa => { password => "Circle Of Life", }, + }, + }, + }, + } + }); sub foo : Local { my ( $self, $c ) = @_; - $c->authorization_required( realm => "foo" ); # named after the status code ;-) - + $c->authenticate({ realm => "example" }); # either user gets authenticated or 401 is sent + # Note that the authentication realm sent to the client is overridden + # here, but this does not affect the Catalyst::Authentication::Realm + # used for authentication. do_stuff(); } - - # with ACL plugin - __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate_http }); - - sub end : Private { + + sub always_auth : Local { my ( $self, $c ) = @_; - - $c->authorization_required_response( realm => "foo" ); - $c->error(0); + + # Force authorization headers onto the response so that the user + # is asked again for authentication, even if they successfully + # authenticated. + my $realm = $c->get_auth_realm('example'); + $realm->credential->authorization_required_response($c, $realm); } + # with ACL plugin + __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate }); + =head1 DESCRIPTION This module lets you use HTTP authentication with @@ -435,78 +451,102 @@ C methods as shown below. =over 4 -=item authorization_required %opts +=item new $config, $c, $realm -Tries to C, and if that fails calls -C and detaches the current action call stack. +Simple constructor. -This method just passes the options through untouched. +=item authenticate $c, $realm, \%auth_info -=item authenticate_http %opts +Tries to authenticate the user, and if that fails calls +C and detaches the current action call stack. Looks inside C<< $c->request->headers >> and processes the digest and basic (badly named) authorization header. This will only try the methods set in the configuration. First digest, then basic. -See the next two methods for what %opts can contain. +The %auth_info hash can contain a number of keys which control the authentication behaviour: + +=over + +=item realm + +Sets the HTTP authentication realm presented to the client. Note this does not alter the +Catalyst::Authentication::Realm object used for the authentication. -=item authenticate_basic %opts +=item domain -=item authenticate_digest %opts +Array reference to domains used to build the authorization headers. -Try to authenticate one of the methods without checking if the method is -allowed in the configuration. +=back + +=item authenticate_basic $c, $realm, \%auth_info + +Performs HTTP basic authentication. + +=item authenticate_digest $c, $realm, \%auth_info + +Performs HTTP digest authentication. Note that the password_type B by I for +digest authentication to succeed, and you must have L in +your application as digest authentication needs to store persistent data. -%opts can contain C (either an object or a name), C (to disregard -%the username from the header altogether, overriding it with a username or user -%object). +Note - if you do not want to store your user passwords as clear text, then it is possible +to store instead the MD5 digest in hex of the string '$username:$realm:$password' -=item authorization_required_response %opts +=item authorization_required_response $c, $realm, \%auth_info Sets C<< $c->response >> to the correct status code, and adds the correct header to demand authentication data from the user agent. -Typically used by C, but may be invoked manually. +Typically used by C, but may be invoked manually. -%opts can contain C, C and C, which are used to build +%opts can contain C and C, which are used to build %the digest header. -=item store_digest_authorization_nonce $key, $nonce +=item store_digest_authorization_nonce $c, $key, $nonce -=item get_digest_authorization_nonce $key +=item get_digest_authorization_nonce $c, $key Set or get the C<$nonce> object used by the digest auth mode. You may override these methods. By default they will call C and C on C<< $c->cache >>. -=item get_http_auth_store %opts - =back =head1 CONFIGURATION -All configuration is stored in C<< YourApp->config->{authentication}{http} >>. +All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>. This should be a hash, and it can contain the following entries: -=over 4 - -=item store - -Either a name or an object -- the default store to use for HTTP authentication. +=over =item type Can be either C (the default), C or C. -This controls C and C, but +This controls C and C, but not the "manual" methods. =item authorization_required_message -Set this to a string to override the default body content "Authorization required." +Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated. + +=item password_type + +The type of password returned by the user object. Same usage as in +L + +=item password_field + +The name of accessor used to retrieve the value of the password field from the user object. Same usage as in +L + +=item use_uri_for + +If this configuration key has a true value, then the domain(s) for the authorization header will be +run through $c->uri_for() =back @@ -521,13 +561,20 @@ C methods return a hashed or salted version of the password. =head1 AUTHORS -Yuval Kogman, C +Updated to current name space and currently maintained +by: Tomas Doran C. -Jess Robinson +Original module by: -Sascha Kiefer C +=over -Tomas Doran C +=item Yuval Kogman, C + +=item Jess Robinson + +=item Sascha Kiefer C + +=back =head1 SEE ALSO @@ -535,7 +582,7 @@ RFC 2617 (or its successors), L, L