5 package Catalyst::Plugin::Authentication::Credential::HTTP;
7 use base qw/Catalyst::Plugin::Authentication::Credential::Password/;
17 use String::Escape ();
27 our $VERSION = "0.05";
31 sub authenticate_http {
37 return $c->authenticate_digest || $c->authenticate_basic;
43 sub authenticate_basic {
49 $c->log->debug('Checking http basic authentication.') if $c->debug;
53 my $headers = $c->req->headers;
57 if ( my ( $user, $password ) = $headers->authorization_basic ) {
61 if ( my $store = $c->config->{authentication}{http}{store} ) {
63 $user = $store->get_user($user);
69 return $c->login( $user, $password );
81 sub authenticate_digest {
87 $c->log->debug('Checking http digest authentication.') if $c->debug;
91 my $headers = $c->req->headers;
93 my @authorization = $headers->header('Authorization');
95 foreach my $authorization (@authorization) {
97 next unless $authorization =~ m{^Digest};
107 my @key_val = split /=/, $_, 2;
109 $key_val[0] = lc $key_val[0];
111 $key_val[1] =~ s{"}{}g; # remove the quotes
115 } split /,\s?/, substr( $authorization, 7 ); #7 == length "Digest "
119 my $opaque = $res{opaque};
121 my $nonce = $c->cache->get( __PACKAGE__ . '::opaque:' . $opaque );
127 $c->log->debug('Checking authentication parameters.')
133 my $uri = '/' . $c->request->path;
135 my $algorithm = $res{algorithm} || 'MD5';
137 my $nonce_count = '0x' . $res{nc};
141 my $check = $uri eq $res{uri}
143 && ( exists $res{username} )
145 && ( exists $res{qop} )
147 && ( exists $res{cnonce} )
149 && ( exists $res{nc} )
151 && $algorithm eq $nonce->algorithm
153 && hex($nonce_count) > hex( $nonce->nonce_count )
155 && $res{nonce} eq $nonce->nonce; # TODO: set Stale instead
161 $c->log->debug('Digest authentication failed. Bad request.')
165 $c->res->status(400); # bad request
167 die $Catalyst::DETACH;
173 $c->log->debug('Checking authentication response.')
179 my $username = $res{username};
181 my $realm = $res{realm};
187 my $store = $c->config->{authentication}{http}{store}
189 || $c->default_auth_store;
191 $user = $store->get_user($username) if $store;
193 unless ($user) { # no user, no authentication
195 $c->log->debug('Unknown user: $user.') if $c->debug;
203 # everything looks good, let's check the response
207 # calculate H(A2) as per spec
209 my $ctx = Digest::MD5->new;
211 $ctx->add( join( ':', $c->request->method, $res{uri} ) );
213 if ( $res{qop} eq 'auth-int' ) {
217 Digest::MD5::md5_hex( $c->request->body ); # not sure here
219 $ctx->add( ':', $digest );
223 my $A2_digest = $ctx->hexdigest;
227 # the idea of the for loop:
229 # if we do not want to store the plain password in our user store,
231 # we can store md5_hex("$username:$realm:$password") instead
233 for my $r ( 0 .. 1 ) {
237 # calculate H(A1) as per spec
239 my $A1_digest = $r ? $user->password : do {
241 $ctx = Digest::MD5->new;
243 $ctx->add( join( ':', $username, $realm, $user->password ) );
249 if ( $nonce->algorithm eq 'MD5-sess' ) {
251 $ctx = Digest::MD5->new;
253 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
255 $A1_digest = $ctx->hexdigest;
261 my $rq_digest = Digest::MD5::md5_hex(
265 $A1_digest, $res{nonce},
267 $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
275 $nonce->nonce_count($nonce_count);
277 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
283 return $c->login( $user, $user->password )
285 if $rq_digest eq $res{response};
305 die "A cache is needed for http digest authentication."
307 unless $c->can('cache');
315 my ( $c, $type ) = @_;
319 my $cfgtype = lc( $c->config->{authentication}{http}{type} || 'any' );
321 return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
329 sub authorization_required {
331 my ( $c, %opts ) = @_;
335 return 1 if $c->_is_auth_type('digest') && $c->authenticate_digest;
337 return 1 if $c->_is_auth_type('basic') && $c->authenticate_basic;
341 $c->authorization_required_response(%opts);
345 die $Catalyst::DETACH;
351 sub authorization_required_response {
353 my ( $c, %opts ) = @_;
357 $c->res->status(401);
361 my ( $digest, $basic );
363 $digest = $c->build_authorization_required_response( \%opts, 'Digest' )
365 if $c->_is_auth_type('digest');
367 $basic = $c->build_authorization_required_response( \%opts, 'Basic' )
369 if $c->_is_auth_type('basic');
373 die 'Could not build authorization required response. '
375 . 'Did you configure a valid authentication http type: '
377 . 'basic, digest, any'
379 unless $digest || $basic;
383 $c->res->headers->push_header( 'WWW-Authenticate' => $digest )
387 $c->res->headers->push_header( 'WWW-Authenticate' => $basic ) if $basic;
393 sub build_authorization_required_response {
395 my ( $c, $opts, $type ) = @_;
401 if ( my $realm = $opts->{realm} ) {
403 push @opts, 'realm=' . String::Escape::qprintable($realm);
409 if ( my $domain = $opts->{domain} ) {
411 Catalyst::Excpetion->throw("domain must be an array reference")
413 unless ref($domain) && ref($domain) eq "ARRAY";
419 $c->config->{authentication}{http}{use_uri_for}
421 ? ( map { $c->uri_for($_) } @$domain )
423 : ( map { URI::Escape::uri_escape($_) } @$domain );
427 push @opts, qq{domain="@uris"};
433 if ( $type eq 'Digest' ) {
435 my $package = __PACKAGE__ . '::Nonce';
437 my $nonce = $package->new;
439 $nonce->algorithm( $c->config->{authentication}{http}{algorithm}
441 || $nonce->algorithm );
445 push @opts, 'qop="' . $nonce->qop . '"';
447 push @opts, 'nonce="' . $nonce->nonce . '"';
449 push @opts, 'opaque="' . $nonce->opaque . '"';
451 push @opts, 'algorithm="' . $nonce->algorithm . '"';
457 $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque, $nonce );
463 return "$type " . join( ', ', @opts );
469 package Catalyst::Plugin::Authentication::Credential::HTTP::Nonce;
475 use base qw[ Class::Accessor::Fast ];
481 our $VERSION = "0.01";
485 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
493 my $self = $class->SUPER::new(@_);
497 $self->nonce( Data::UUID->new->create_b64 );
499 $self->opaque( Data::UUID->new->create_b64 );
501 $self->qop('auth,auth-int');
503 $self->nonce_count('0x0');
505 $self->algorithm('MD5');
531 Catalyst::Plugin::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
545 Authentication::Store::Moose
547 Authentication::Credential::HTTP
553 __PACKAGE__->config->{authentication}{http}{type} = 'any'; # or 'digest' or 'basic'
554 __PACKAGE__->config->{authentication}{users} = {
555 Mufasa => { password => "Circle Of Life", },
562 my ( $self, $c ) = @_;
566 $c->authorization_required( realm => "foo" ); # named after the status code ;-)
570 # either user gets authenticated or 401 is sent
582 __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate_http });
588 my ( $self, $c ) = @_;
592 $c->authorization_required_response( realm => "foo" );
604 This moduule lets you use HTTP authentication with
606 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
608 are currently supported.
620 =item authorization_required
624 Tries to C<authenticate_http>, and if that fails calls
626 C<authorization_required_response> and detaches the current action call stack.
630 =item authenticate_http
634 Looks inside C<< $c->request->headers >> and processes the digest and basic
636 (badly named) authorization header.
640 =item authorization_required_response
644 Sets C<< $c->response >> to the correct status code, and adds the correct
646 header to demand authentication data from the user agent.
658 Yuval Kogman, C<nothingmuch@woobling.org>
666 Sascha Kiefer C<esskar@cpan.org>
670 =head1 COPYRIGHT & LICENSE
674 Copyright (c) 2005-2006 the aforementioned authors. All rights
676 reserved. This program is free software; you can redistribute
678 it and/or modify it under the same terms as Perl itself.