1 package Catalyst::Authentication::Credential::HTTP;
2 use base qw/Catalyst::Authentication::Credential::Password/;
13 __PACKAGE__->mk_accessors(qw/_config realm/);
16 our $VERSION = "1.004";
19 my ($class, $config, $app, $realm) = @_;
21 my $self = { _config => $config, _debug => $app->debug };
32 my $type = $self->_config->{'type'} ||= 'any';
34 if (!grep /$type/, ('basic', 'digest', 'any')) {
35 Catalyst::Exception->throw(__PACKAGE__ . " used with unsupported authentication type: " . $type);
40 my ( $self, $c, $realm, $auth_info ) = @_;
43 $auth = $self->authenticate_digest($c, $realm, $auth_info) if $self->_is_http_auth_type('digest');
44 return $auth if $auth;
46 $auth = $self->authenticate_basic($c, $realm, $auth_info) if $self->_is_http_auth_type('basic');
47 return $auth if $auth;
49 $self->authorization_required_response($c, $realm, $auth_info);
50 die $Catalyst::DETACH;
53 sub authenticate_basic {
54 my ( $self, $c, $realm, $auth_info ) = @_;
56 $c->log->debug('Checking http basic authentication.') if $c->debug;
58 my $headers = $c->req->headers;
60 if ( my ( $username, $password ) = $headers->authorization_basic ) {
61 my $user_obj = $realm->find_user( { username => $username }, $c);
63 if ($self->check_password($user_obj, {$self->_config->{password_field} => $password})) {
64 $c->set_authenticated($user_obj);
69 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
77 sub authenticate_digest {
78 my ( $self, $c, $realm, $auth_info ) = @_;
80 $c->log->debug('Checking http digest authentication.') if $c->debug;
82 my $headers = $c->req->headers;
83 my @authorization = $headers->header('Authorization');
84 foreach my $authorization (@authorization) {
85 next unless $authorization =~ m{^Digest};
87 my @key_val = split /=/, $_, 2;
88 $key_val[0] = lc $key_val[0];
89 $key_val[1] =~ s{"}{}g; # remove the quotes
91 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
93 my $opaque = $res{opaque};
94 my $nonce = $self->get_digest_authorization_nonce( $c, __PACKAGE__ . '::opaque:' . $opaque );
97 $c->log->debug('Checking authentication parameters.')
100 my $uri = '/' . $c->request->path;
101 my $algorithm = $res{algorithm} || 'MD5';
102 my $nonce_count = '0x' . $res{nc};
104 my $check = $uri eq $res{uri}
105 && ( exists $res{username} )
106 && ( exists $res{qop} )
107 && ( exists $res{cnonce} )
108 && ( exists $res{nc} )
109 && $algorithm eq $nonce->algorithm
110 && hex($nonce_count) > hex( $nonce->nonce_count )
111 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
114 $c->log->debug('Digest authentication failed. Bad request.')
116 $c->res->status(400); # bad request
117 Carp::confess $Catalyst::DETACH;
120 $c->log->debug('Checking authentication response.')
123 my $username = $res{username};
127 unless ( $user = $auth_info->{user} ) {
128 $user = $realm->find_user( { username => $username }, $c);
130 unless ($user) { # no user, no authentication
131 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
135 # everything looks good, let's check the response
136 # calculate H(A2) as per spec
137 my $ctx = Digest::MD5->new;
138 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
139 if ( $res{qop} eq 'auth-int' ) {
141 Digest::MD5::md5_hex( $c->request->body ); # not sure here
142 $ctx->add( ':', $digest );
144 my $A2_digest = $ctx->hexdigest;
146 # the idea of the for loop:
147 # if we do not want to store the plain password in our user store,
148 # we can store md5_hex("$username:$realm:$password") instead
149 my $password_field = $self->_config->{password_field};
150 for my $r ( 0 .. 1 ) {
151 # calculate H(A1) as per spec
152 my $A1_digest = $r ? $user->$password_field() : do {
153 $ctx = Digest::MD5->new;
154 $ctx->add( join( ':', $username, $realm->name, $user->$password_field() ) );
157 if ( $nonce->algorithm eq 'MD5-sess' ) {
158 $ctx = Digest::MD5->new;
159 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
160 $A1_digest = $ctx->hexdigest;
163 my $digest_in = join( ':',
164 $A1_digest, $res{nonce},
165 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
167 my $rq_digest = Digest::MD5::md5_hex($digest_in);
168 $nonce->nonce_count($nonce_count);
169 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
171 if ($rq_digest eq $res{response}) {
172 $c->set_authenticated($user);
183 die "A cache is needed for http digest authentication."
184 unless $c->can('cache');
188 sub _is_http_auth_type {
189 my ( $self, $type ) = @_;
190 my $cfgtype = lc( $self->_config->{'type'} || 'any' );
191 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
195 sub authorization_required_response {
196 my ( $self, $c, $realm, $auth_info ) = @_;
198 $c->res->status(401);
199 $c->res->content_type('text/plain');
200 if (exists $self->_config->{authorization_required_message}) {
201 # If you set the key to undef, don't stamp on the body.
202 $c->res->body($self->_config->{authorization_required_message})
203 if defined $c->res->body($self->_config->{authorization_required_message});
206 $c->res->body('Authorization required.');
209 # *DONT* short circuit
211 $ok++ if $self->_create_digest_auth_response($c, $auth_info);
212 $ok++ if $self->_create_basic_auth_response($c, $auth_info);
215 die 'Could not build authorization required response. '
216 . 'Did you configure a valid authentication http type: '
217 . 'basic, digest, any';
222 sub _add_authentication_header {
223 my ( $c, $header ) = @_;
224 $c->response->headers->push_header( 'WWW-Authenticate' => $header );
228 sub _create_digest_auth_response {
229 my ( $self, $c, $opts ) = @_;
231 return unless $self->_is_http_auth_type('digest');
233 if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
234 _add_authentication_header( $c, $digest );
241 sub _create_basic_auth_response {
242 my ( $self, $c, $opts ) = @_;
244 return unless $self->_is_http_auth_type('basic');
246 if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
247 _add_authentication_header( $c, $basic );
254 sub _build_auth_header_realm {
255 my ( $self, $c, $opts ) = @_;
256 if ( my $realm_name = String::Escape::qprintable($opts->{realm} ? $opts->{realm} : $self->realm->name) ) {
257 $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
258 return 'realm=' . $realm_name;
263 sub _build_auth_header_domain {
264 my ( $self, $c, $opts ) = @_;
265 if ( my $domain = $opts->{domain} ) {
266 Catalyst::Exception->throw("domain must be an array reference")
267 unless ref($domain) && ref($domain) eq "ARRAY";
270 $self->_config->{use_uri_for}
271 ? ( map { $c->uri_for($_) } @$domain )
272 : ( map { URI::Escape::uri_escape($_) } @$domain );
274 return qq{domain="@uris"};
279 sub _build_auth_header_common {
280 my ( $self, $c, $opts ) = @_;
282 $self->_build_auth_header_realm($c, $opts),
283 $self->_build_auth_header_domain($c, $opts),
287 sub _build_basic_auth_header {
288 my ( $self, $c, $opts ) = @_;
289 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
292 sub _build_digest_auth_header {
293 my ( $self, $c, $opts ) = @_;
295 my $nonce = $self->_digest_auth_nonce($c, $opts);
297 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
299 $self->store_digest_authorization_nonce( $c, $key, $nonce );
301 return _join_auth_header_parts( Digest =>
302 $self->_build_auth_header_common($c, $opts),
303 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
312 sub _digest_auth_nonce {
313 my ( $self, $c, $opts ) = @_;
315 my $package = __PACKAGE__ . '::Nonce';
317 my $nonce = $package->new;
319 if ( my $algorithm = $opts->{algorithm} || $self->_config->{algorithm}) {
320 $nonce->algorithm( $algorithm );
326 sub _join_auth_header_parts {
327 my ( $type, @parts ) = @_;
328 return "$type " . join(", ", @parts );
331 sub get_digest_authorization_nonce {
332 my ( $self, $c, $key ) = @_;
335 return $c->cache->get( $key );
338 sub store_digest_authorization_nonce {
339 my ( $self, $c, $key, $nonce ) = @_;
342 return $c->cache->set( $key, $nonce );
345 package Catalyst::Authentication::Credential::HTTP::Nonce;
348 use base qw[ Class::Accessor::Fast ];
351 our $VERSION = '0.02';
353 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
357 my $self = $class->SUPER::new(@_);
359 $self->nonce( Data::UUID->new->create_b64 );
360 $self->opaque( Data::UUID->new->create_b64 );
361 $self->qop('auth,auth-int');
362 $self->nonce_count('0x0');
363 $self->algorithm('MD5');
376 Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
385 __PACKAGE__->config( authentication => {
390 type => 'any', # or 'digest' or 'basic'
391 password_type => 'clear',
392 password_field => 'password'
397 Mufasa => { password => "Circle Of Life", },
405 my ( $self, $c ) = @_;
407 $c->authenticate({ realm => "example" });
408 # either user gets authenticated or 401 is sent
409 # Note that the authentication realm sent to the client is overridden
410 # here, but this does not affect the Catalyst::Authentication::Realm
411 # used for authentication.
416 sub always_auth : Local {
417 my ( $self, $c ) = @_;
419 # Force authorization headers onto the response so that the user
420 # is asked again for authentication, even if they successfully
422 my $realm = $c->get_auth_realm('example');
423 $realm->credential->authorization_required_response($c, $realm);
427 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
431 This module lets you use HTTP authentication with
432 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
433 are currently supported.
435 When authentication is required, this module sets a status of 401, and
436 the body of the response to 'Authorization required.'. To override
437 this and set your own content, check for the C<< $c->res->status ==
438 401 >> in your C<end> action, and change the body accordingly.
446 A nonce is a one-time value sent with each digest authentication
447 request header. The value must always be unique, so per default the
448 last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
449 change this behaviour, override the
450 C<store_digest_authorization_nonce> and
451 C<get_digest_authorization_nonce> methods as shown below.
459 =item new $config, $c, $realm
465 Validates that $config is ok.
467 =item authenticate $c, $realm, \%auth_info
469 Tries to authenticate the user, and if that fails calls
470 C<authorization_required_response> and detaches the current action call stack.
472 Looks inside C<< $c->request->headers >> and processes the digest and basic
473 (badly named) authorization header.
475 This will only try the methods set in the configuration. First digest, then basic.
477 The %auth_info hash can contain a number of keys which control the authentication behaviour:
483 Sets the HTTP authentication realm presented to the client. Note this does not alter the
484 Catalyst::Authentication::Realm object used for the authentication.
488 Array reference to domains used to build the authorization headers.
490 This list of domains defines the protection space. If a domain URI is an
491 absolute path (starts with /), it is relative to the root URL of the server being accessed.
492 An absolute URI in this list may refer to a different server than the one being accessed.
494 The client will use this list to determine the set of URIs for which the same authentication
495 information may be sent.
497 If this is omitted or its value is empty, the client will assume that the
498 protection space consists of all URIs on the responding server.
500 Therefore, if your application is not hosted at the root of this domain, and you want to
501 prevent the authentication credentials for this application being sent to any other applications.
502 then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
506 =item authenticate_basic $c, $realm, \%auth_info
508 Performs HTTP basic authentication.
510 =item authenticate_digest $c, $realm, \%auth_info
512 Performs HTTP digest authentication. Note that the password_type B<must> by I<clear> for
513 digest authentication to succeed, and you must have L<Catalyst::Plugin::Session> in
514 your application as digest authentication needs to store persistent data.
516 Note - if you do not want to store your user passwords as clear text, then it is possible
517 to store instead the MD5 digest in hex of the string '$username:$realm:$password'
519 Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
520 and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
522 =item authorization_required_response $c, $realm, \%auth_info
524 Sets C<< $c->response >> to the correct status code, and adds the correct
525 header to demand authentication data from the user agent.
527 Typically used by C<authenticate>, but may be invoked manually.
529 %opts can contain C<domain> and C<algorithm>, which are used to build
532 =item store_digest_authorization_nonce $c, $key, $nonce
534 =item get_digest_authorization_nonce $c, $key
536 Set or get the C<$nonce> object used by the digest auth mode.
538 You may override these methods. By default they will call C<get> and C<set> on
545 All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
547 This should be a hash, and it can contain the following entries:
553 Can be either C<any> (the default), C<basic> or C<digest>.
555 This controls C<authorization_required_response> and C<authenticate>, but
556 not the "manual" methods.
558 =item authorization_required_message
560 Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
564 The type of password returned by the user object. Same usage as in
565 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/passwprd_type>
569 The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
570 L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
574 If this configuration key has a true value, then the domain(s) for the authorization header will be
575 run through $c->uri_for(). Use this configuration option if your application is not running at the root
576 of your domain, and you want to ensure that authentication credentials from your application are not shared with
577 other applications on the same server.
583 When using digest authentication, this module will only work together
584 with authentication stores whose User objects have a C<password>
585 method that returns the plain-text password. It will not work together
586 with L<Catalyst::Authentication::Store::Htpasswd>, or
587 L<Catalyst::Authentication::Store::DBIC> stores whose
588 C<password> methods return a hashed or salted version of the password.
592 Updated to current name space and currently maintained
593 by: Tomas Doran C<bobtfish@bobtfish.net>.
599 =item Yuval Kogman, C<nothingmuch@woobling.org>
603 =item Sascha Kiefer C<esskar@cpan.org>
609 RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
611 =head1 COPYRIGHT & LICENSE
613 Copyright (c) 2005-2008 the aforementioned authors. All rights
614 reserved. This program is free software; you can redistribute
615 it and/or modify it under the same terms as Perl itself.