Version 1.014
[catagits/Catalyst-Authentication-Credential-HTTP.git] / lib / Catalyst / Authentication / Credential / HTTP.pm
index aa04326..85d6418 100644 (file)
@@ -9,53 +9,102 @@ use URI::Escape    ();
 use Catalyst       ();
 use Digest::MD5    ();
 
-our $VERSION = "0.11";
+__PACKAGE__->mk_accessors(qw/
+    _config
+    authorization_required_message
+    password_field
+    username_field
+    type
+    realm
+    algorithm
+    use_uri_for
+    no_unprompted_authorization_required
+    require_ssl
+/);
+
+our $VERSION = '1.014';
 
-sub authenticate_http {
-    my ( $c, @args ) = @_;
+sub new {
+    my ($class, $config, $app, $realm) = @_;
 
-    return 1 if $c->_is_http_auth_type('digest') && $c->authenticate_digest(@args);
-    return 1 if $c->_is_http_auth_type('basic')  && $c->authenticate_basic(@args);
-    return;
+    $config->{username_field} ||= 'username';
+    # _config is shity back-compat with our base class.
+    my $self = { %$config, _config => $config, _debug => $app->debug ? 1 : 0 };
+    bless $self, $class;
+
+    $self->realm($realm);
+
+    $self->init;
+    return $self;
 }
 
-sub get_http_auth_store {
-    my ( $c, %opts ) = @_;
+sub init {
+    my ($self) = @_;
+    my $type = $self->type || 'any';
 
-    my $store = $opts{store} || $c->config->{authentication}{http}{store} || return;
+    if (!grep /$type/, ('basic', 'digest', 'any')) {
+        Catalyst::Exception->throw(__PACKAGE__ . " used with unsupported authentication type: " . $type);
+    }
+    $self->type($type);
+}
 
-    return ref $store
-        ? $store
-        : $c->get_auth_store($store);
+sub authenticate {
+    my ( $self, $c, $realm, $auth_info ) = @_;
+    my $auth;
+
+    $self->authentication_failed( $c, $realm, $auth_info )
+        if $self->require_ssl ? $c->req->base->scheme ne 'https' : 0;
+
+    $auth = $self->authenticate_digest($c, $realm, $auth_info) if $self->_is_http_auth_type('digest');
+    return $auth if $auth;
+
+    $auth = $self->authenticate_basic($c, $realm, $auth_info) if $self->_is_http_auth_type('basic');
+    return $auth if $auth;
+
+    $self->authentication_failed( $c, $realm, $auth_info );
+}
+
+sub authentication_failed {
+    my ( $self, $c, $realm, $auth_info ) = @_;
+    unless ($self->no_unprompted_authorization_required) {
+        $self->authorization_required_response($c, $realm, $auth_info);
+        die $Catalyst::DETACH;
+    }
 }
 
 sub authenticate_basic {
-    my ( $c, %opts ) = @_;
+    my ( $self, $c, $realm, $auth_info ) = @_;
 
     $c->log->debug('Checking http basic authentication.') if $c->debug;
 
     my $headers = $c->req->headers;
 
     if ( my ( $username, $password ) = $headers->authorization_basic ) {
-
-        my $user;
-
-        unless ( $user = $opts{user} ) {
-            if ( my $store = $c->get_http_auth_store(%opts) ) {
-                $user = $store->get_user($username);
-            } else {
-                $user = $username;
+           my $user_obj = $realm->find_user( { $self->username_field => $username }, $c);
+           if (ref($user_obj)) {
+            my $opts = {};
+            $opts->{$self->password_field} = $password
+                if $self->password_field;
+            if ($self->check_password($user_obj, $opts)) {
+                return $user_obj;
             }
+            else {
+                $c->log->debug("Password mismatch!") if $c->debug;
+                return;
+            }
+         }
+         else {
+             $c->log->debug("Unable to locate user matching user info provided")
+                if $c->debug;
+            return;
         }
-
-        return $c->login( $user, $password );
     }
 
-    return 0;
+    return;
 }
 
 sub authenticate_digest {
-    my ( $c, %opts ) = @_;
+    my ( $self, $c, $realm, $auth_info ) = @_;
 
     $c->log->debug('Checking http digest authentication.') if $c->debug;
 
@@ -63,7 +112,6 @@ sub authenticate_digest {
     my @authorization = $headers->header('Authorization');
     foreach my $authorization (@authorization) {
         next unless $authorization =~ m{^Digest};
-
         my %res = map {
             my @key_val = split /=/, $_, 2;
             $key_val[0] = lc $key_val[0];
@@ -72,13 +120,13 @@ sub authenticate_digest {
         } split /,\s?/, substr( $authorization, 7 );    #7 == length "Digest "
 
         my $opaque = $res{opaque};
-        my $nonce  = $c->get_digest_authorization_nonce( __PACKAGE__ . '::opaque:' . $opaque );
+        my $nonce  = $self->get_digest_authorization_nonce( $c, __PACKAGE__ . '::opaque:' . $opaque );
         next unless $nonce;
 
         $c->log->debug('Checking authentication parameters.')
           if $c->debug;
 
-        my $uri         = '/' . $c->request->path;
+        my $uri         = $c->request->uri->path_query;
         my $algorithm   = $res{algorithm} || 'MD5';
         my $nonce_count = '0x' . $res{nc};
 
@@ -95,30 +143,25 @@ sub authenticate_digest {
             $c->log->debug('Digest authentication failed. Bad request.')
               if $c->debug;
             $c->res->status(400);             # bad request
-            die $Catalyst::DETACH;
+            Carp::confess $Catalyst::DETACH;
         }
 
         $c->log->debug('Checking authentication response.')
           if $c->debug;
 
         my $username = $res{username};
-        my $realm    = $res{realm};
 
-        my $user;
+        my $user_obj;
 
-        unless ( $user = $opts{user} ) {
-            if ( my $store = $c->get_http_auth_store(%opts) || $c->default_auth_store ) {
-                $user = $store->get_user($username);
-            }
+        unless ( $user_obj = $auth_info->{user} ) {
+            $user_obj = $realm->find_user( { $self->username_field => $username }, $c);
         }
-
-        unless ($user) {    # no user, no authentication
-            $c->log->debug('Unknown user: $user.') if $c->debug;
-            return 0;
+        unless ($user_obj) {    # no user, no authentication
+            $c->log->debug("Unable to locate user matching user info provided") if $c->debug;
+            return;
         }
 
         # everything looks good, let's check the response
-
         # calculate H(A2) as per spec
         my $ctx = Digest::MD5->new;
         $ctx->add( join( ':', $c->request->method, $res{uri} ) );
@@ -132,12 +175,12 @@ sub authenticate_digest {
         # the idea of the for loop:
         # if we do not want to store the plain password in our user store,
         # we can store md5_hex("$username:$realm:$password") instead
+        my $password_field = $self->password_field;
         for my $r ( 0 .. 1 ) {
-
             # calculate H(A1) as per spec
-            my $A1_digest = $r ? $user->password : do {
+            my $A1_digest = $r ? $user_obj->$password_field() : do {
                 $ctx = Digest::MD5->new;
-                $ctx->add( join( ':', $username, $realm, $user->password ) );
+                $ctx->add( join( ':', $username, $realm->name, $user_obj->$password_field() ) );
                 $ctx->hexdigest;
             };
             if ( $nonce->algorithm eq 'MD5-sess' ) {
@@ -146,23 +189,20 @@ sub authenticate_digest {
                 $A1_digest = $ctx->hexdigest;
             }
 
-            my $rq_digest = Digest::MD5::md5_hex(
-                join( ':',
+            my $digest_in = join( ':',
                     $A1_digest, $res{nonce},
                     $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
-                    $A2_digest )
-            );
-
+                    $A2_digest );
+            my $rq_digest = Digest::MD5::md5_hex($digest_in);
             $nonce->nonce_count($nonce_count);
-            $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
-                $nonce );
-
-            return $c->login( $user, $user->password )
-              if $rq_digest eq $res{response};
+            my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
+            $self->store_digest_authorization_nonce( $c, $key, $nonce );
+            if ($rq_digest eq $res{response}) {
+                return $user_obj;
+            }
         }
     }
-
-    return 0;
+    return;
 }
 
 sub _check_cache {
@@ -174,36 +214,30 @@ sub _check_cache {
 }
 
 sub _is_http_auth_type {
-    my ( $c, $type ) = @_;
-
-    my $cfgtype = lc( $c->config->{authentication}{http}{type} || 'any' );
+    my ( $self, $type ) = @_;
+    my $cfgtype = lc( $self->type );
     return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
     return 0;
 }
 
-sub authorization_required {
-    my ( $c, @args ) = @_;
-
-    return 1 if $c->authenticate_http(@args);
-    
-    $c->authorization_required_response(@args);
-
-    die $Catalyst::DETACH;
-}
-
 sub authorization_required_response {
-    my ( $c, %opts ) = @_;
+    my ( $self, $c, $realm, $auth_info ) = @_;
 
     $c->res->status(401);
     $c->res->content_type('text/plain');
-    $c->res->body($c->config->{authentication}{http}{authorization_required_message} || 
-                  $opts{authorization_required_message} || 
-                  'Authorization required.');
+    if (exists $self->{authorization_required_message}) {
+        # If you set the key to undef, don't stamp on the body.
+        $c->res->body($self->authorization_required_message)
+            if defined $self->authorization_required_message;
+    }
+    else {
+        $c->res->body('Authorization required.');
+    }
 
     # *DONT* short circuit
     my $ok;
-    $ok++ if $c->_create_digest_auth_response(\%opts);
-    $ok++ if $c->_create_basic_auth_response(\%opts);
+    $ok++ if $self->_create_digest_auth_response($c, $auth_info);
+    $ok++ if $self->_create_basic_auth_response($c, $auth_info);
 
     unless ( $ok ) {
         die 'Could not build authorization required response. '
@@ -215,17 +249,17 @@ sub authorization_required_response {
 
 sub _add_authentication_header {
     my ( $c, $header ) = @_;
-    $c->res->headers->push_header( 'WWW-Authenticate' => $header );
+    $c->response->headers->push_header( 'WWW-Authenticate' => $header );
     return;
 }
 
 sub _create_digest_auth_response {
-    my ( $c, $opts ) = @_;
-      
-    return unless $c->_is_http_auth_type('digest');
-    
-    if ( my $digest = $c->_build_digest_auth_header( $opts ) ) {
-        $c->_add_authentication_header( $digest );
+    my ( $self, $c, $opts ) = @_;
+
+    return unless $self->_is_http_auth_type('digest');
+
+    if ( my $digest = $self->_build_digest_auth_header( $c, $opts ) ) {
+        _add_authentication_header( $c, $digest );
         return 1;
     }
 
@@ -233,12 +267,12 @@ sub _create_digest_auth_response {
 }
 
 sub _create_basic_auth_response {
-    my ( $c, $opts ) = @_;
-    
-    return unless $c->_is_http_auth_type('basic');
+    my ( $self, $c, $opts ) = @_;
+
+    return unless $self->_is_http_auth_type('basic');
 
-    if ( my $basic = $c->_build_basic_auth_header( $opts ) ) {
-        $c->_add_authentication_header( $basic );
+    if ( my $basic = $self->_build_basic_auth_header( $c, $opts ) ) {
+        _add_authentication_header( $c, $basic );
         return 1;
     }
 
@@ -246,56 +280,54 @@ sub _create_basic_auth_response {
 }
 
 sub _build_auth_header_realm {
-    my ( $c, $opts ) = @_;    
-
-    if ( my $realm = $opts->{realm} ) {
-        return 'realm=' . String::Escape::qprintable($realm);
-    } 
+    my ( $self, $c, $opts ) = @_;
+    if ( my $realm_name = String::Escape::qprintable($opts->{realm} ? $opts->{realm} : $self->realm->name) ) {
+        $realm_name = qq{"$realm_name"} unless $realm_name =~ /^"/;
+        return 'realm=' . $realm_name;
+    }
     return;
 }
 
 sub _build_auth_header_domain {
-    my ( $c, $opts ) = @_;
-
+    my ( $self, $c, $opts ) = @_;
     if ( my $domain = $opts->{domain} ) {
         Catalyst::Exception->throw("domain must be an array reference")
           unless ref($domain) && ref($domain) eq "ARRAY";
 
         my @uris =
-          $c->config->{authentication}{http}{use_uri_for}
+          $self->use_uri_for
           ? ( map { $c->uri_for($_) } @$domain )
           : ( map { URI::Escape::uri_escape($_) } @$domain );
 
         return qq{domain="@uris"};
-    } 
+    }
     return;
 }
 
 sub _build_auth_header_common {
-    my ( $c, $opts ) = @_;
-
+    my ( $self, $c, $opts ) = @_;
     return (
-        $c->_build_auth_header_realm($opts),
-        $c->_build_auth_header_domain($opts),
+        $self->_build_auth_header_realm($c, $opts),
+        $self->_build_auth_header_domain($c, $opts),
     );
 }
 
 sub _build_basic_auth_header {
-    my ( $c, $opts ) = @_;
-    return $c->_join_auth_header_parts( Basic => $c->_build_auth_header_common( $opts ) );
+    my ( $self, $c, $opts ) = @_;
+    return _join_auth_header_parts( Basic => $self->_build_auth_header_common( $c, $opts ) );
 }
 
 sub _build_digest_auth_header {
-    my ( $c, $opts ) = @_;
+    my ( $self, $c, $opts ) = @_;
 
-    my $nonce = $c->_digest_auth_nonce($opts);
+    my $nonce = $self->_digest_auth_nonce($c, $opts);
 
     my $key = __PACKAGE__ . '::opaque:' . $nonce->opaque;
-   
-    $c->store_digest_authorization_nonce( $key, $nonce );
 
-    return $c->_join_auth_header_parts( Digest =>
-        $c->_build_auth_header_common($opts),
+    $self->store_digest_authorization_nonce( $c, $key, $nonce );
+
+    return _join_auth_header_parts( Digest =>
+        $self->_build_auth_header_common($c, $opts),
         map { sprintf '%s="%s"', $_, $nonce->$_ } qw(
             qop
             nonce
@@ -306,13 +338,13 @@ sub _build_digest_auth_header {
 }
 
 sub _digest_auth_nonce {
-    my ( $c, $opts ) = @_;
+    my ( $self, $c, $opts ) = @_;
 
     my $package = __PACKAGE__ . '::Nonce';
 
     my $nonce   = $package->new;
 
-    if ( my $algorithm = $opts->{algorithm} || $c->config->{authentication}{http}{algorithm}) { 
+    if ( my $algorithm = $opts->{algorithm} || $self->algorithm) {
         $nonce->algorithm( $algorithm );
     }
 
@@ -320,21 +352,21 @@ sub _digest_auth_nonce {
 }
 
 sub _join_auth_header_parts {
-    my ( $c, $type, @parts ) = @_;
+    my ( $type, @parts ) = @_;
     return "$type " . join(", ", @parts );
 }
 
 sub get_digest_authorization_nonce {
-    my ( $c, $key ) = @_;
+    my ( $self, $c, $key ) = @_;
 
-    $c->_check_cache;
+    _check_cache($c);
     return $c->cache->get( $key );
 }
 
 sub store_digest_authorization_nonce {
-    my ( $c, $key, $nonce ) = @_;
+    my ( $self, $c, $key, $nonce ) = @_;
 
-    $c->_check_cache;
+    _check_cache($c);
     return $c->cache->set( $key, $nonce );
 }
 
@@ -376,35 +408,55 @@ for Catalyst.
 
     use Catalyst qw/
         Authentication
-        Authentication::Store::Minimal
-        Authentication::Credential::HTTP
     /;
 
-    __PACKAGE__->config->{authentication}{http}{type} = 'any'; # or 'digest' or 'basic'
-    __PACKAGE__->config->{authentication}{users} = {
-        Mufasa => { password => "Circle Of Life", },
-    };
+    __PACKAGE__->config( authentication => {
+        default_realm => 'example',
+        realms => {
+            example => {
+                credential => {
+                    class => 'HTTP',
+                    type  => 'any', # or 'digest' or 'basic'
+                    password_type  => 'clear',
+                    password_field => 'password'
+                },
+                store => {
+                    class => 'Minimal',
+                    users => {
+                        Mufasa => { password => "Circle Of Life", },
+                    },
+                },
+            },
+        }
+    });
 
     sub foo : Local {
         my ( $self, $c ) = @_;
 
-        $c->authorization_required( realm => "foo" ); # named after the status code ;-)
-
+        $c->authenticate({ realm => "example" });
         # either user gets authenticated or 401 is sent
+        # Note that the authentication realm sent to the client (in the
+        # RFC 2617 sense) is overridden here, but this *does not*
+        # effect the Catalyst::Authentication::Realm used for
+        # authentication - to do that, you need
+        # $c->authenticate({}, 'otherrealm')
 
         do_stuff();
     }
 
-    # with ACL plugin
-    __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate_http });
-
-    sub end : Private {
+    sub always_auth : Local {
         my ( $self, $c ) = @_;
 
-        $c->authorization_required_response( realm => "foo" );
-        $c->error(0);
+        # Force authorization headers onto the response so that the user
+        # is asked again for authentication, even if they successfully
+        # authenticated.
+        my $realm = $c->get_auth_realm('example');
+        $realm->credential->authorization_required_response($c, $realm);
     }
 
+    # with ACL plugin
+    __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate });
+
 =head1 DESCRIPTION
 
 This module lets you use HTTP authentication with
@@ -435,78 +487,155 @@ C<get_digest_authorization_nonce> methods as shown below.
 
 =over 4
 
-=item authorization_required %opts
+=item new $config, $c, $realm
 
-Tries to C<authenticate_http>, and if that fails calls
-C<authorization_required_response> and detaches the current action call stack.
+Simple constructor.
 
-This method just passes the options through untouched.
+=item init
 
-=item authenticate_http %opts
+Validates that $config is ok.
+
+=item authenticate $c, $realm, \%auth_info
+
+Tries to authenticate the user, and if that fails calls
+C<authorization_required_response> and detaches the current action call stack.
 
 Looks inside C<< $c->request->headers >> and processes the digest and basic
 (badly named) authorization header.
 
 This will only try the methods set in the configuration. First digest, then basic.
 
-See the next two methods for what %opts can contain.
+The %auth_info hash can contain a number of keys which control the authentication behaviour:
 
-=item authenticate_basic %opts
+=over
 
-=item authenticate_digest %opts
+=item realm
 
-Try to authenticate one of the methods without checking if the method is
-allowed in the configuration.
+Sets the HTTP authentication realm presented to the client. Note this does not alter the
+Catalyst::Authentication::Realm object used for the authentication.
 
-%opts can contain C<store> (either an object or a name), C<user> (to disregard
-%the username from the header altogether, overriding it with a username or user
-%object).
+=item domain
 
-=item authorization_required_response %opts
+Array reference to domains used to build the authorization headers.
+
+This list of domains defines the protection space. If a domain URI is an
+absolute path (starts with /), it is relative to the root URL of the server being accessed.
+An absolute URI in this list may refer to a different server than the one being accessed.
+
+The client will use this list to determine the set of URIs for which the same authentication
+information may be sent.
+
+If this is omitted or its value is empty, the client will assume that the
+protection space consists of all URIs on the responding server.
+
+Therefore, if your application is not hosted at the root of this domain, and you want to
+prevent the authentication credentials for this application being sent to any other applications.
+then you should use the I<use_uri_for> configuration option, and pass a domain of I</>.
+
+=back
+
+=item authenticate_basic $c, $realm, \%auth_info
+
+Performs HTTP basic authentication.
+
+=item authenticate_digest $c, $realm, \%auth_info
+
+Performs HTTP digest authentication.
+
+The password_type B<must> be I<clear> for digest authentication to
+succeed.  If you do not want to store your user passwords as clear
+text, you may instead store the MD5 digest in hex of the string
+'$username:$realm:$password'.
+
+L<Catalyst::Plugin::Cache> is used for persistent storage of the nonce
+values (see L</Nonce>).  It must be loaded in your application, unless
+you override the C<store_digest_authorization_nonce> and
+C<get_digest_authorization_nonce> methods as shown below.
+
+Takes an additional parameter of I<algorithm>, the possible values of which are 'MD5' (the default)
+and 'MD5-sess'. For more information about 'MD5-sess', see section 3.2.2.2 in RFC 2617.
+
+=item authorization_required_response $c, $realm, \%auth_info
 
 Sets C<< $c->response >> to the correct status code, and adds the correct
 header to demand authentication data from the user agent.
 
-Typically used by C<authorization_required>, but may be invoked manually.
+Typically used by C<authenticate>, but may be invoked manually.
 
-%opts can contain C<realm>, C<domain> and C<algorithm>, which are used to build
+%opts can contain C<domain> and C<algorithm>, which are used to build
 %the digest header.
 
-=item store_digest_authorization_nonce $key, $nonce
+=item store_digest_authorization_nonce $c, $key, $nonce
 
-=item get_digest_authorization_nonce $key
+=item get_digest_authorization_nonce $c, $key
 
 Set or get the C<$nonce> object used by the digest auth mode.
 
 You may override these methods. By default they will call C<get> and C<set> on
 C<< $c->cache >>.
 
-=item get_http_auth_store %opts
+=item authentication_failed
+
+Sets the 401 response and calls C<< $ctx->detach >>.
 
 =back
 
 =head1 CONFIGURATION
 
-All configuration is stored in C<< YourApp->config->{authentication}{http} >>.
+All configuration is stored in C<< YourApp->config('Plugin::Authentication' => { yourrealm => { credential => { class => 'HTTP', %config } } } >>.
 
 This should be a hash, and it can contain the following entries:
 
-=over 4
-
-=item store
-
-Either a name or an object -- the default store to use for HTTP authentication.
+=over
 
 =item type
 
 Can be either C<any> (the default), C<basic> or C<digest>.
 
-This controls C<authorization_required_response> and C<authenticate_http>, but
+This controls C<authorization_required_response> and C<authenticate>, but
 not the "manual" methods.
 
 =item authorization_required_message
 
-Set this to a string to override the default body content "Authorization required."
+Set this to a string to override the default body content "Authorization required.", or set to undef to suppress body content being generated.
+
+=item password_type
+
+The type of password returned by the user object. Same usage as in
+L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_type>
+
+=item password_field
+
+The name of accessor used to retrieve the value of the password field from the user object. Same usage as in
+L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password/password_field>
+
+=item username_field
+
+The field name that the user's username is mapped into when finding the user from the realm. Defaults to 'username'.
+
+=item use_uri_for
+
+If this configuration key has a true value, then the domain(s) for the authorization header will be
+run through $c->uri_for(). Use this configuration option if your application is not running at the root
+of your domain, and you want to ensure that authentication credentials from your application are not shared with
+other applications on the same server.
+
+=item require_ssl
+
+If this configuration key has a true value then authentication will be denied
+(and a 401 issued in normal circumstances) unless the request is via https.
+
+=item no_unprompted_authorization_required
+
+Causes authentication to fail as normal modules do, without calling
+C<< $c->detach >>. This means that the basic auth credential can be used as
+part of the progressive realm.
+
+However use like this is probably not optimum it also means that users in
+browsers ill never get a HTTP authenticate dialogue box (unless you manually
+return a 410 response in your application), and even some automated
+user agents (for APIs) will not send the Authorization header without
+specific manipulation of the request headers.
 
 =back
 
@@ -521,13 +650,32 @@ C<password> methods return a hashed or salted version of the password.
 
 =head1 AUTHORS
 
-Yuval Kogman, C<nothingmuch@woobling.org>
+Updated to current name space and currently maintained
+by: Tomas Doran C<bobtfish@bobtfish.net>.
+
+Original module by:
+
+=over
+
+=item Yuval Kogman, C<nothingmuch@woobling.org>
+
+=item Jess Robinson
+
+=item Sascha Kiefer C<esskar@cpan.org>
 
-Jess Robinson
+=back
+
+=head1 CONTRIBUTORS
+
+Patches contributed by:
 
-Sascha Kiefer C<esskar@cpan.org>
+=over
 
-Tomas Doran C<bobtfish@bobtfish.net>
+=item Peter Corlett
+
+=item Devin Austin (dhoss) C<dhoss@cpan.org>
+
+=back
 
 =head1 SEE ALSO
 
@@ -535,7 +683,7 @@ RFC 2617 (or its successors), L<Catalyst::Plugin::Cache>, L<Catalyst::Plugin::Au
 
 =head1 COPYRIGHT & LICENSE
 
-        Copyright (c) 2005-2006 the aforementioned authors. All rights
+        Copyright (c) 2005-2008 the aforementioned authors. All rights
         reserved. This program is free software; you can redistribute
         it and/or modify it under the same terms as Perl itself.