1 package Catalyst::Authentication::Credential::HTTP;
2 use base qw/Catalyst::Authentication::Credential::Password/;
12 our $VERSION = "0.11";
14 sub authenticate_http {
15 my ( $c, @args ) = @_;
17 return 1 if $c->_is_http_auth_type('digest') && $c->authenticate_digest(@args);
18 return 1 if $c->_is_http_auth_type('basic') && $c->authenticate_basic(@args);
22 sub get_http_auth_store {
23 my ( $c, %opts ) = @_;
25 my $store = $opts{store} || $c->config->{authentication}{http}{store} || return;
29 : $c->get_auth_store($store);
32 sub authenticate_basic {
33 my ( $c, %opts ) = @_;
35 $c->log->debug('Checking http basic authentication.') if $c->debug;
37 my $headers = $c->req->headers;
39 if ( my ( $username, $password ) = $headers->authorization_basic ) {
43 unless ( $user = $opts{user} ) {
44 if ( my $store = $c->get_http_auth_store(%opts) ) {
45 $user = $store->get_user($username);
51 return $c->login( $user, $password );
57 sub authenticate_digest {
58 my ( $c, %opts ) = @_;
60 $c->log->debug('Checking http digest authentication.') if $c->debug;
62 my $headers = $c->req->headers;
63 my @authorization = $headers->header('Authorization');
64 foreach my $authorization (@authorization) {
65 next unless $authorization =~ m{^Digest};
68 my @key_val = split /=/, $_, 2;
69 $key_val[0] = lc $key_val[0];
70 $key_val[1] =~ s{"}{}g; # remove the quotes
72 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
74 my $opaque = $res{opaque};
75 my $nonce = $c->get_digest_authorization_nonce( __PACKAGE__ . '::opaque:' . $opaque );
78 $c->log->debug('Checking authentication parameters.')
81 my $uri = '/' . $c->request->path;
82 my $algorithm = $res{algorithm} || 'MD5';
83 my $nonce_count = '0x' . $res{nc};
85 my $check = $uri eq $res{uri}
86 && ( exists $res{username} )
87 && ( exists $res{qop} )
88 && ( exists $res{cnonce} )
89 && ( exists $res{nc} )
90 && $algorithm eq $nonce->algorithm
91 && hex($nonce_count) > hex( $nonce->nonce_count )
92 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
95 $c->log->debug('Digest authentication failed. Bad request.')
97 $c->res->status(400); # bad request
98 die $Catalyst::DETACH;
101 $c->log->debug('Checking authentication response.')
104 my $username = $res{username};
105 my $realm = $res{realm};
109 unless ( $user = $opts{user} ) {
110 if ( my $store = $c->get_http_auth_store(%opts) || $c->default_auth_store ) {
111 $user = $store->get_user($username);
115 unless ($user) { # no user, no authentication
116 $c->log->debug('Unknown user: $user.') if $c->debug;
120 # everything looks good, let's check the response
122 # calculate H(A2) as per spec
123 my $ctx = Digest::MD5->new;
124 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
125 if ( $res{qop} eq 'auth-int' ) {
127 Digest::MD5::md5_hex( $c->request->body ); # not sure here
128 $ctx->add( ':', $digest );
130 my $A2_digest = $ctx->hexdigest;
132 # the idea of the for loop:
133 # if we do not want to store the plain password in our user store,
134 # we can store md5_hex("$username:$realm:$password") instead
135 for my $r ( 0 .. 1 ) {
137 # calculate H(A1) as per spec
138 my $A1_digest = $r ? $user->password : do {
139 $ctx = Digest::MD5->new;
140 $ctx->add( join( ':', $username, $realm, $user->password ) );
143 if ( $nonce->algorithm eq 'MD5-sess' ) {
144 $ctx = Digest::MD5->new;
145 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
146 $A1_digest = $ctx->hexdigest;
149 my $rq_digest = Digest::MD5::md5_hex(
151 $A1_digest, $res{nonce},
152 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
156 $nonce->nonce_count($nonce_count);
157 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
160 return $c->login( $user, $user->password )
161 if $rq_digest eq $res{response};
171 die "A cache is needed for http digest authentication."
172 unless $c->can('cache');
176 sub _is_http_auth_type {
177 my ( $c, $type ) = @_;
179 my $cfgtype = lc( $c->config->{authentication}{http}{type} || 'any' );
180 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
184 sub authorization_required {
185 my ( $c, @args ) = @_;
187 return 1 if $c->authenticate_http(@args);
189 $c->authorization_required_response(@args);
191 die $Catalyst::DETACH;
194 sub authorization_required_response {
195 my ( $c, %opts ) = @_;
197 $c->res->status(401);
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.');
203 # *DONT* short circuit
205 $ok++ if $c->_create_digest_auth_response(\%opts);
206 $ok++ if $c->_create_basic_auth_response(\%opts);
209 die 'Could not build authorization required response. '
210 . 'Did you configure a valid authentication http type: '
211 . 'basic, digest, any';
216 sub _add_authentication_header {
217 my ( $c, $header ) = @_;
218 $c->res->headers->push_header( 'WWW-Authenticate' => $header );
222 sub _create_digest_auth_response {
223 my ( $c, $opts ) = @_;
225 return unless $c->_is_http_auth_type('digest');
227 if ( my $digest = $c->_build_digest_auth_header( $opts ) ) {
228 $c->_add_authentication_header( $digest );
235 sub _create_basic_auth_response {
236 my ( $c, $opts ) = @_;
238 return unless $c->_is_http_auth_type('basic');
240 if ( my $basic = $c->_build_basic_auth_header( $opts ) ) {
241 $c->_add_authentication_header( $basic );
248 sub _build_auth_header_realm {
249 my ( $c, $opts ) = @_;
251 if ( my $realm = $opts->{realm} ) {
252 return 'realm=' . String::Escape::qprintable($realm);
257 sub _build_auth_header_domain {
258 my ( $c, $opts ) = @_;
260 if ( my $domain = $opts->{domain} ) {
261 Catalyst::Exception->throw("domain must be an array reference")
262 unless ref($domain) && ref($domain) eq "ARRAY";
265 $c->config->{authentication}{http}{use_uri_for}
266 ? ( map { $c->uri_for($_) } @$domain )
267 : ( map { URI::Escape::uri_escape($_) } @$domain );
269 return qq{domain="@uris"};
274 sub _build_auth_header_common {
275 my ( $c, $opts ) = @_;
278 $c->_build_auth_header_realm($opts),
279 $c->_build_auth_header_domain($opts),
283 sub _build_basic_auth_header {
284 my ( $c, $opts ) = @_;
285 return $c->_join_auth_header_parts( Basic => $c->_build_auth_header_common( $opts ) );
288 sub _build_digest_auth_header {
289 my ( $c, $opts ) = @_;
291 my $nonce = $c->_digest_auth_nonce($opts);
293 my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
295 $c->store_digest_authorization_nonce( $key, $nonce );
297 return $c->_join_auth_header_parts( Digest =>
298 $c->_build_auth_header_common($opts),
299 map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
308 sub _digest_auth_nonce {
309 my ( $c, $opts ) = @_;
311 my $package = __PACKAGE__ . '::Nonce';
313 my $nonce = $package->new;
315 if ( my $algorithm = $opts->{algorithm} || $c->config->{authentication}{http}{algorithm}) {
316 $nonce->algorithm( $algorithm );
322 sub _join_auth_header_parts {
323 my ( $c, $type, @parts ) = @_;
324 return "$type " . join(", ", @parts );
327 sub get_digest_authorization_nonce {
328 my ( $c, $key ) = @_;
331 return $c->cache->get( $key );
334 sub store_digest_authorization_nonce {
335 my ( $c, $key, $nonce ) = @_;
338 return $c->cache->set( $key, $nonce );
341 package Catalyst::Authentication::Credential::HTTP::Nonce;
344 use base qw[ Class::Accessor::Fast ];
347 our $VERSION = '0.02';
349 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
353 my $self = $class->SUPER::new(@_);
355 $self->nonce( Data::UUID->new->create_b64 );
356 $self->opaque( Data::UUID->new->create_b64 );
357 $self->qop('auth,auth-int');
358 $self->nonce_count('0x0');
359 $self->algorithm('MD5');
372 Catalyst::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
379 Authentication::Store::Minimal
380 Authentication::Credential::HTTP
383 __PACKAGE__->config->{authentication}{http}{type} = 'any'; # or 'digest' or 'basic'
384 __PACKAGE__->config->{authentication}{users} = {
385 Mufasa => { password => "Circle Of Life", },
389 my ( $self, $c ) = @_;
391 $c->authorization_required( realm => "foo" ); # named after the status code ;-)
393 # either user gets authenticated or 401 is sent
399 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate_http });
402 my ( $self, $c ) = @_;
404 $c->authorization_required_response( realm => "foo" );
410 This module lets you use HTTP authentication with
411 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
412 are currently supported.
414 When authentication is required, this module sets a status of 401, and
415 the body of the response to 'Authorization required.'. To override
416 this and set your own content, check for the C<< $c->res->status ==
417 401 >> in your C<end> action, and change the body accordingly.
425 A nonce is a one-time value sent with each digest authentication
426 request header. The value must always be unique, so per default the
427 last value of the nonce is kept using L<Catalyst::Plugin::Cache>. To
428 change this behaviour, override the
429 C<store_digest_authorization_nonce> and
430 C<get_digest_authorization_nonce> methods as shown below.
438 =item authorization_required %opts
440 Tries to C<authenticate_http>, and if that fails calls
441 C<authorization_required_response> and detaches the current action call stack.
443 This method just passes the options through untouched.
445 =item authenticate_http %opts
447 Looks inside C<< $c->request->headers >> and processes the digest and basic
448 (badly named) authorization header.
450 This will only try the methods set in the configuration. First digest, then basic.
452 See the next two methods for what %opts can contain.
454 =item authenticate_basic %opts
456 =item authenticate_digest %opts
458 Try to authenticate one of the methods without checking if the method is
459 allowed in the configuration.
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
465 =item authorization_required_response %opts
467 Sets C<< $c->response >> to the correct status code, and adds the correct
468 header to demand authentication data from the user agent.
470 Typically used by C<authorization_required>, but may be invoked manually.
472 %opts can contain C<realm>, C<domain> and C<algorithm>, which are used to build
475 =item store_digest_authorization_nonce $key, $nonce
477 =item get_digest_authorization_nonce $key
479 Set or get the C<$nonce> object used by the digest auth mode.
481 You may override these methods. By default they will call C<get> and C<set> on
484 =item get_http_auth_store %opts
490 All configuration is stored in C<< YourApp->config->{authentication}{http} >>.
492 This should be a hash, and it can contain the following entries:
498 Either a name or an object -- the default store to use for HTTP authentication.
502 Can be either C<any> (the default), C<basic> or C<digest>.
504 This controls C<authorization_required_response> and C<authenticate_http>, but
505 not the "manual" methods.
507 =item authorization_required_message
509 Set this to a string to override the default body content "Authorization required."
515 When using digest authentication, this module will only work together
516 with authentication stores whose User objects have a C<password>
517 method that returns the plain-text password. It will not work together
518 with L<Catalyst::Authentication::Store::Htpasswd>, or
519 L<Catalyst::Authentication::Store::DBIC> stores whose
520 C<password> methods return a hashed or salted version of the password.
524 Yuval Kogman, C<nothingmuch@woobling.org>
528 Sascha Kiefer C<esskar@cpan.org>
530 Tomas Doran C<bobtfish@bobtfish.net>
534 RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Authentication>
536 =head1 COPYRIGHT & LICENSE
538 Copyright (c) 2005-2006 the aforementioned authors. All rights
539 reserved. This program is free software; you can redistribute
540 it and/or modify it under the same terms as Perl itself.