HTTP basic auth
Yuval Kogman [Sun, 4 Dec 2005 22:08:45 +0000 (22:08 +0000)]
Build.PL [new file with mode: 0644]
MANIFEST.SKIP [new file with mode: 0644]
lib/Catalyst/Plugin/Authentication/Credential/HTTP.pm [new file with mode: 0644]
t/basic.t [new file with mode: 0644]

diff --git a/Build.PL b/Build.PL
new file mode 100644 (file)
index 0000000..6b03b4d
--- /dev/null
+++ b/Build.PL
@@ -0,0 +1,19 @@
+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;
+
diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP
new file mode 100644 (file)
index 0000000..20d203b
--- /dev/null
@@ -0,0 +1,26 @@
+# 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]$
diff --git a/lib/Catalyst/Plugin/Authentication/Credential/HTTP.pm b/lib/Catalyst/Plugin/Authentication/Credential/HTTP.pm
new file mode 100644 (file)
index 0000000..75fee7a
--- /dev/null
@@ -0,0 +1,135 @@
+#!/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
+
+
diff --git a/t/basic.t b/t/basic.t
new file mode 100644 (file)
index 0000000..84d6822
--- /dev/null
+++ b/t/basic.t
@@ -0,0 +1,58 @@
+#!/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");