--- /dev/null
+use strict;
+use Module::Build;
+
+my $build = Module::Build->new(
+ create_makefile_pl => 'traditional',
+ license => 'perl',
+ module_name => 'Catalyst::Plugin::Authentication::Credential::HTTP',
+ requires => {
+ 'perl' => '5.8.1',
+ 'Catalyst' => '5.49',
+ 'Catalyst::Plugin::Authentication' => 0,
+ 'String::Escape' => 0,
+ 'URI::Escape' => 0,
+ },
+ create_readme => 1,
+ sign => 1,
+);
+$build->create_build_script;
+
--- /dev/null
+# Avoid version control files.
+\bRCS\b
+\bCVS\b
+,v$
+\B\.svn\b
+
+# Avoid Makemaker generated and utility files.
+\bMakefile$
+\bblib
+\bMakeMaker-\d
+\bpm_to_blib$
+\bblibdirs$
+^MANIFEST\.SKIP$
+
+# Avoid Module::Build generated and utility files.
+\bBuild$
+\b_build
+
+# Avoid temp and backup files.
+~$
+\.tmp$
+\.old$
+\.bak$
+\#$
+\b\.#
+^..*\.sw[po]$
--- /dev/null
+#!/usr/bin/perl
+
+package Catalyst::Plugin::Authentication::Credential::HTTP;
+use base qw/Catalyst::Plugin::Authentication::Credential::Password/;
+
+use strict;
+use warnings;
+
+use String::Escape ();
+use URI::Escape ();
+
+our $VERSION = "0.01";
+
+sub authenticate_http {
+ my $c = shift;
+
+ my $headers = $c->req->headers;
+
+ if ( my ( $user, $password ) = $headers->authorization_basic ) {
+
+ if ( my $store = $c->config->{authentication}{http}{store} ) {
+ $user = $store->get_user($user);
+ }
+
+ return $c->login( $user, $password );
+ }
+}
+
+sub authorization_required {
+ my ( $c, %opts ) = @_;
+
+ return 1 if $c->authenticate_http;
+
+ $c->authorization_required_response( %opts );
+
+ $c->detach( sub { } );
+}
+
+sub authorization_required_response {
+ my ( $c, %opts ) = @_;
+
+ $c->res->status(401);
+
+ my @opts;
+
+ if ( my $realm = $opts{realm} ) {
+ push @opts, sprintf 'realm=%s', String::Escape::qprintable($realm);
+ }
+
+ if ( my $domain = $opts{domain} ) {
+ Catalyst::Excpetion->throw("domain must be an array reference")
+ unless ref($domain) && ref($domain) eq "ARRAY";
+
+ my @uris =
+ $c->config->{authentication}{http}{use_uri_for}
+ ? ( map { $c->uri_for($_) } @$domain )
+ : ( map { URI::Escape::uri_escape($_) } @$domain );
+
+ push @opts, qq{domain="@uris"};
+ }
+
+ $c->res->headers->www_authenticate(join " ", "Basic", @opts);
+}
+
+__PACKAGE__;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+Catalyst::Plugin::Authentication::Credential::HTTP - HTTP Basic authentication
+for Catlayst.
+
+=head1 SYNOPSIS
+
+ use Catalyst qw/
+ Authentication
+ Authentication::Store::Moose
+ Authentication::Credential::HTTP
+ /;
+
+ sub foo : Local {
+ my ( $self, $c ) = @_;
+
+ $c->authorization_requried( realm => "foo" ); # named after the status code ;-)
+
+ # either user gets authenticated or 401 is sent
+
+ do_stuff();
+ }
+
+ # with ACL plugin
+ __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate_http });
+
+ sub end : Private {
+ my ( $self, $c ) = @_;
+
+ $c->authorization_required_response( realm => "foo" );
+ $c->error(0);
+ }
+
+=head1 DESCRIPTION
+
+This moduule lets you use HTTP authentication with
+L<Catalyst::Plugin::Authentication>.
+
+Currently this module only supports the Basic scheme, but upon request Digest
+will also be added. Patches welcome!
+
+=head1 METHODS
+
+=over 4
+
+=item authorization_required
+
+Tries to C<authenticate_http>, and if that files calls
+C<authorization_required_response> and detaches the current action call stack.
+
+=item authenticate_http
+
+Looks inside C<< $c->request->headers >> and processes the basic (badly named)
+authorization header.
+
+=item authorization_required_response
+
+Sets C<< $c->response >> to the correct status code, and adds the correct
+header to demand authentication data from the user agent.
+
+=back
+
+=cut
+
+
--- /dev/null
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 13;
+use Test::MockObject::Extends;
+use Test::MockObject;
+use HTTP::Headers;
+
+
+my $m; BEGIN { use_ok($m = "Catalyst::Plugin::Authentication::Credential::HTTP") }
+
+can_ok( $m, "authenticate_http" );
+can_ok( $m, "authorization_required" );
+can_ok( $m, "authorization_required_response" );
+
+my $req = Test::MockObject->new;
+my $req_headers = HTTP::Headers->new;
+
+$req->set_always( headers => $req_headers );
+
+my $res = Test::MockObject->new;
+
+my $status;
+$res->mock(status => sub { $status = $_[1] });
+
+my $res_headers = HTTP::Headers->new;
+$res->set_always( headers => $res_headers );
+
+my $c = Test::MockObject::Extends->new( $m );
+
+my @login_info;
+$c->mock( login => sub { shift; @login_info = @_; 1 } );
+$c->set_false( "detach" );
+$c->set_always( config => {} );
+$c->set_always( req => $req );
+$c->set_always( res => $res );
+
+
+ok( !$c->authenticate_http, "http auth fails without header");
+
+$req_headers->authorization_basic( qw/foo bar/ );
+
+ok( $c->authenticate_http, "auth successful with header");
+is_deeply( \@login_info, [qw/foo bar/], "login info delegated");
+
+ok( $c->authorization_required, "authorization required with successful authentication");
+ok( !$c->called("detach"), "didnt' detach");
+
+$req_headers->clear;
+$c->clear;
+
+ok( !$c->authorization_required, "authorization required with bad authentication");
+$c->called_ok("detach", "detached");
+
+is( $status, 401, "401 status code" );
+like( $res_headers->www_authenticate, qr/^Basic/, "WWW-Authenticate header set");