1 package Catalyst::Authentication::Credential::HTTP;
2 use base qw/Catalyst::Component/;
13 __PACKAGE__->mk_accessors(qw/_config realm/);
16 our $VERSION = "1.000";
19 my ($class, $config, $app, $realm) = @_;
21 my $self = { _config => $config, _debug => $app->debug };
26 my $type = $self->_config->{'type'} ||= 'any';
28 if (!grep /$type/, ('basic', 'digest', 'any')) {
29 Catalyst::Exception->throw(__PACKAGE__ . " used with unsupported authentication type: " . $type);
35 my ( $self, $c, $realm, $auth_info ) = @_;
38 $auth = $self->authenticate_digest($c, $realm, $auth_info) if $self->_is_http_auth_type('digest');
39 return $auth if $auth;
41 $auth = $self->authenticate_basic($c, $realm, $auth_info) if $self->_is_http_auth_type('basic');
42 return $auth if $auth;
44 $self->authorization_required_response($c, $realm, $auth_info);
45 die $Catalyst::DETACH;
48 sub authenticate_basic {
49 my ( $self, $c, $realm, $auth_info ) = @_;
51 $c->log->debug('Checking http basic authentication.') if $c->debug;
53 my $headers = $c->req->headers;
55 if ( my ( $username, $password ) = $headers->authorization_basic ) {
56 my $user_obj = $realm->find_user( { username => $username }, $c);
58 if ($user_obj->check_password($password)) {
59 $c->set_authenticated($user_obj);
64 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
72 sub authenticate_digest {
73 my ( $self, $c, $realm, $auth_info ) = @_;
75 $c->log->debug('Checking http digest authentication.') if $c->debug;
77 my $headers = $c->req->headers;
78 my @authorization = $headers->header('Authorization');
79 foreach my $authorization (@authorization) {
80 next unless $authorization =~ m{^Digest};
82 my @key_val = split /=/, $_, 2;
83 $key_val[0] = lc $key_val[0];
84 $key_val[1] =~ s{"}{}g; # remove the quotes
86 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
88 my $opaque = $res{opaque};
89 my $nonce = $self->get_digest_authorization_nonce( $c, __PACKAGE__ . '::opaque:' . $opaque );
92 $c->log->debug('Checking authentication parameters.')
95 my $uri = '/' . $c->request->path;
96 my $algorithm = $res{algorithm} || 'MD5';
97 my $nonce_count = '0x' . $res{nc};
99 my $check = $uri eq $res{uri}
100 && ( exists $res{username} )
101 && ( exists $res{qop} )
102 && ( exists $res{cnonce} )
103 && ( exists $res{nc} )
104 && $algorithm eq $nonce->algorithm
105 && hex($nonce_count) > hex( $nonce->nonce_count )
106 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
109 $c->log->debug('Digest authentication failed. Bad request.')
111 $c->res->status(400); # bad request
112 Carp::confess $Catalyst::DETACH;
115 $c->log->debug('Checking authentication response.')
118 my $username = $res{username};
122 unless ( $user = $auth_info->{user} ) {
123 $user = $realm->find_user( { username => $username }, $c);
125 unless ($user) { # no user, no authentication
126 $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
130 # everything looks good, let's check the response
131 # calculate H(A2) as per spec
132 my $ctx = Digest::MD5->new;
133 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
134 if ( $res{qop} eq 'auth-int' ) {
136 Digest::MD5::md5_hex( $c->request->body ); # not sure here
137 $ctx->add( ':', $digest );
139 my $A2_digest = $ctx->hexdigest;
141 # the idea of the for loop:
142 # if we do not want to store the plain password in our user store,
143 # we can store md5_hex("$username:$realm:$password") instead
144 for my $r ( 0 .. 1 ) {
146 # calculate H(A1) as per spec
147 my $A1_digest = $r ? $user->password : do {
148 $ctx = Digest::MD5->new;
149 $ctx->add( join( ':', $username, $realm->name, $user->password ) );
152 if ( $nonce->algorithm eq 'MD5-sess' ) {
153 $ctx = Digest::MD5->new;
154 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
155 $A1_digest = $ctx->hexdigest;
158 my $digest_in = join( ':',
159 $A1_digest, $res{nonce},
160 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
162 my $rq_digest = Digest::MD5::md5_hex($digest_in);
163 $nonce->nonce_count($nonce_count);
164 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
166 if ($rq_digest eq $res{response}) {
167 $c->set_authenticated($user);
178 die "A cache is needed for http digest authentication."
179 unless $c->can('cache');
183 sub _is_http_auth_type {
184 my ( $self, $type ) = @_;
185 my $cfgtype = lc( $self->_config->{'type'} || 'any' );
186 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
190 sub authorization_required_response {
191 my ( $self, $c, $realm, $auth_info ) = @_;
193 $c->res->status(401);
194 $c->res->content_type('text/plain');
195 if (exists $self->_config->{authorization_required_message}) {
196 # If you set the key to undef, don't stamp on the body.
197 $c->res->body($self->_config->{authorization_required_message})
198 if defined $c->res->body($self->_config->{authorization_required_message});
201 $c->res->body('Authorization required.');
204 # *DONT* short circuit
206 $ok++ if $self->_create_digest_auth_response($c, $auth_info);
207 $ok++ if $self->_create_basic_auth_response($c, $auth_info);
210 die 'Could not build authorization required response. '
211 . 'Did you configure a valid authentication http type: '
212 . 'basic, digest, any';
217 sub _add_authentication_header {
218 my ( $c, $header ) = @_;
219 $c->response->headers->push_header( 'WWW-Authenticate' => $header );
223 sub _create_digest_auth_response {
224 my ( $self, $c, $opts ) = @_;
226 return unless $self->_is_http_auth_type('digest');
228 if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
229 _add_authentication_header( $c, $digest );
236 sub _create_basic_auth_response {
237 my ( $self, $c, $opts ) = @_;
239 return unless $self->_is_http_auth_type('basic');
241 if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
242 _add_authentication_header( $c, $basic );
249 sub _build_auth_header_realm {
252 if ( my $realm = $self->realm ) {
253 my $realm_name = String::Escape::qprintable($realm->name);
254 $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
255 return 'realm=' . $realm_name;
260 sub _build_auth_header_domain {
261 my ( $self, $c, $opts ) = @_;
263 if ( my $domain = $opts->{domain} ) {
264 Catalyst::Exception->throw("domain must be an array reference")
265 unless ref($domain) && ref($domain) eq "ARRAY";
268 $self->_config->{use_uri_for}
269 ? ( map { $c->uri_for($_) } @$domain )
270 : ( map { URI::Escape::uri_escape($_) } @$domain );
272 return qq{domain="@uris"};
277 sub _build_auth_header_common {
278 my ( $self, $c, $opts ) = @_;
281 $self->_build_auth_header_realm(),
282 $self->_build_auth_header_domain($c, $opts),
286 sub _build_basic_auth_header {
287 my ( $self, $c, $opts ) = @_;
288 return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
291 sub _build_digest_auth_header {
292 my ( $self, $c, $opts ) = @_;
294 my $nonce = $self->_digest_auth_nonce($c, $opts);
296 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
298 $self->store_digest_authorization_nonce( $c, $key, $nonce );
300 return _join_auth_header_parts( Digest =>
301 $self->_build_auth_header_common($c, $opts),
302 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
311 sub _digest_auth_nonce {
312 my ( $self, $c, $opts ) = @_;
314 my $package = __PACKAGE__ . '::Nonce';
316 my $nonce = $package->new;
318 if ( my $algorithm = $opts->{algorithm} || $self->_config->{algorithm}) {
319 $nonce->algorithm( $algorithm );
325 sub _join_auth_header_parts {
326 my ( $type, @parts ) = @_;
327 return "$type " . join(", ", @parts );
330 sub get_digest_authorization_nonce {
331 my ( $self, $c, $key ) = @_;
334 return $c->cache->get( $key );
337 sub store_digest_authorization_nonce {
338 my ( $self, $c, $key, $nonce ) = @_;
341 return $c->cache->set( $key, $nonce );
344 package Catalyst::Authentication::Credential::HTTP::Nonce;
347 use base qw[ Class::Accessor::Fast ];
350 our $VERSION = '0.02';
352 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
356 my $self = $class->SUPER::new(@_);
358 $self->nonce( Data::UUID->new->create_b64 );
359 $self->opaque( Data::UUID->new->create_b64 );
360 $self->qop('auth,auth-int');
361 $self->nonce_count('0x0');
362 $self->algorithm('MD5');
375 Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
384 __PACKAGE__->config( authentication => {
389 type => 'any', # or 'digest' or 'basic'
394 Mufasa => { password => "Circle Of Life", },
402 my ( $self, $c ) = @_;
404 $c->authenticate({ realm => "example" });
405 # either user gets authenticated or 401 is sent
411 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
415 This module lets you use HTTP authentication with
416 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
417 are currently supported.
419 When authentication is required, this module sets a status of 401, and
420 the body of the response to 'Authorization required.'. To override
421 this and set your own content, check for the C<< $c->res->status ==
422 401 >> in your C<end> action, and change the body accordingly.
430 A nonce is a one-time value sent with each digest authentication
431 request header. The value must always be unique, so per default the
432 last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
433 change this behaviour, override the
434 C<store_digest_authorization_nonce> and
435 C<get_digest_authorization_nonce> methods as shown below.
443 =item new $config, $c, $realm
447 =item authenticate $c, $realm, \%auth_info
449 Tries to authenticate the user, and if that fails calls
450 C<authorization_required_response> and detaches the current action call stack.
452 Looks inside C<< $c->request->headers >> and processes the digest and basic
453 (badly named) authorization header.
455 This will only try the methods set in the configuration. First digest, then basic.
457 This method just passes the options through untouched. See the next two methods for what \%auth_info can contain.
459 =item authenticate_basic $c, $realm, \%auth_info
461 =item authenticate_digest $c, $realm, \%auth_info
463 Try to authenticate one of the methods without checking if the method is
464 allowed in the configuration.
466 =item authorization_required_response $c, $realm, \%auth_info
468 Sets C<< $c->response >> to the correct status code, and adds the correct
469 header to demand authentication data from the user agent.
471 Typically used by C<authenticate>, but may be invoked manually.
473 %opts can contain C<domain> and C<algorithm>, which are used to build
476 =item store_digest_authorization_nonce $c, $key, $nonce
478 =item get_digest_authorization_nonce $c, $key
480 Set or get the C<$nonce> object used by the digest auth mode.
482 You may override these methods. By default they will call C<get> and C<set> on
489 All configuration is stored in C<< YourApp->config(authentication => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
491 This should be a hash, and it can contain the following entries:
497 Can be either C<any> (the default), C<basic> or C<digest>.
499 This controls C<authorization_required_response> and C<authenticate>, but
500 not the "manual" methods.
502 =item authorization_required_message
504 Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
510 When using digest authentication, this module will only work together
511 with authentication stores whose User objects have a C<password>
512 method that returns the plain-text password. It will not work together
513 with L<Catalyst::Authentication::Store::Htpasswd>, or
514 L<Catalyst::Authentication::Store::DBIC> stores whose
515 C<password> methods return a hashed or salted version of the password.
519 Updated to current name space and currently maintained
520 by: Tomas Doran C<bobtfish@bobtfish.net>.
526 =item Yuval Kogman, C<nothingmuch@woobling.org>
530 =item Sascha Kiefer C<esskar@cpan.org>
536 RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
538 =head1 COPYRIGHT & LICENSE
540 Copyright (c) 2005-2008 the aforementioned authors. All rights
541 reserved. This program is free software; you can redistribute
542 it and/or modify it under the same terms as Perl itself.