X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FCatalyst%2FPlugin%2FAuthentication.pm;h=98415ceed6d146894c0bbbfc38526f795259f8a9;hb=8a31d23d60e1b252399a376007d1f2ffe271890b;hp=ab25422f92737f3715631450ecc91df6743193d2;hpb=a5961476f1c57502011250be04fcba816a03b102;p=catagits%2FCatalyst-Plugin-Authentication.git diff --git a/lib/Catalyst/Plugin/Authentication.pm b/lib/Catalyst/Plugin/Authentication.pm index ab25422..98415ce 100644 --- a/lib/Catalyst/Plugin/Authentication.pm +++ b/lib/Catalyst/Plugin/Authentication.pm @@ -1,43 +1,39 @@ -#!/usr/bin/perl - package Catalyst::Plugin::Authentication; -use base qw/Class::Accessor::Fast Class::Data::Inheritable/; +use base qw/Class::Accessor::Fast/; -BEGIN { - __PACKAGE__->mk_accessors(qw/_user/); - __PACKAGE__->mk_classdata($_) for qw/_auth_stores _auth_store_names/; -} +__PACKAGE__->mk_accessors(qw/_user/); use strict; use warnings; +use MRO::Compat; use Tie::RefHash; use Class::Inspector; +use Catalyst::Authentication::Realm; -# this optimization breaks under Template::Toolkit -# use user_exists instead -#BEGIN { -# require constant; -# constant->import(have_want => eval { require Want }); -#} - -our $VERSION = "0.05"; +our $VERSION = "0.10018"; sub set_authenticated { - my ( $c, $user ) = @_; + my ( $c, $user, $realmname ) = @_; $c->user($user); $c->request->{user} = $user; # compatibility kludge - if ( $c->isa("Catalyst::Plugin::Session") - and $c->config->{authentication}{use_session} - and $user->supports("session") ) - { - $c->save_user_in_session($user); + if (!$realmname) { + $realmname = 'default'; + } + my $realm = $c->get_auth_realm($realmname); + + if (!$realm) { + Catalyst::Exception->throw( + "set_authenticated called with nonexistant realm: '$realmname'."); } + $user->auth_realm($realm->name); - $c->NEXT::set_authenticated($user); + $c->persist_user(); + + $c->maybe::next::method($user, $realmname); } sub user { @@ -47,27 +43,82 @@ sub user { return $c->_user(@_); } - my $user = $c->_user; + if ( defined($c->_user) ) { + return $c->_user; + } else { + return $c->auth_restore_user; + } +} - if ( $user and !Scalar::Util::blessed($user) ) { -# return 1 if have_want() && Want::want("BOOL"); - return $c->auth_restore_user($user); +# change this to allow specification of a realm - to verify the user is part of that realm +# in addition to verifying that they exist. +sub user_exists { + my $c = shift; + return defined($c->_user) || defined($c->find_realm_for_persisted_user); +} + +# works like user_exists - except only returns true if user +# exists AND is in the realm requested. +sub user_in_realm { + my ($c, $realmname) = @_; + + if (defined($c->_user)) { + return ($c->_user->auth_realm eq $realmname); + } else { + my $realm = $c->find_realm_for_persisted_user; + if ($realm) { + return ($realm->name eq $realmname); + } else { + return undef; + } } +} - return $user; +sub __old_save_user_in_session { + my ( $c, $user, $realmname ) = @_; + + $c->session->{__user_realm} = $realmname; + + # we want to ask the store for a user prepared for the session. + # but older modules split this functionality between the user and the + # store. We try the store first. If not, we use the old method. + my $realm = $c->get_auth_realm($realmname); + if ($realm->{'store'}->can('for_session')) { + $c->session->{__user} = $realm->{'store'}->for_session($c, $user); + } else { + $c->session->{__user} = $user->for_session; + } } -sub user_exists { - my $c = shift; - return defined($c->_user); +sub persist_user { + my $c = shift; + + if ($c->user_exists) { + + ## if we have a valid session handler - we store the + ## realm in the session. If not - we have to hope that + ## the realm can recognize its frozen user somehow. + if ($c->can('session') && + $c->config->{'Plugin::Authentication'}{'use_session'} && + $c->session_is_valid) { + + $c->session->{'__user_realm'} = $c->_user->auth_realm; + } + + my $realm = $c->get_auth_realm($c->_user->auth_realm); + + # used to call $realm->save_user_in_session + $realm->persist_user($c, $c->user); + } } -sub save_user_in_session { - my ( $c, $user ) = @_; - my $store = $user->store || ref $user; - $c->session->{__user_store} = $c->get_auth_store_name($store) || $store; - $c->session->{__user} = $user->for_session; +## this was a short lived method to update user information - +## you should use persist_user instead. +sub update_user_in_session { + my $c = shift; + + return $c->persist_user; } sub logout { @@ -75,120 +126,352 @@ sub logout { $c->user(undef); - if ( $c->isa("Catalyst::Plugin::Session") - and $c->config->{authentication}{use_session} ) - { - delete @{ $c->session }{qw/__user __user_store/}; + my $realm = $c->find_realm_for_persisted_user; + if ($realm) { + $realm->remove_persisted_user($c); } - - $c->NEXT::logout(@_); + + $c->maybe::next::method(@_); } -sub get_user { - my ( $c, $uid ) = @_; +sub find_user { + my ( $c, $userinfo, $realmname ) = @_; - if ( my $store = $c->default_auth_store ) { - return $store->get_user($uid); - } - else { + $realmname ||= 'default'; + my $realm = $c->get_auth_realm($realmname); + + if (!$realm) { Catalyst::Exception->throw( - "The user id $uid was passed to an authentication " - . "plugin, but no default store was specified" ); + "find_user called with nonexistant realm: '$realmname'."); } + return $realm->find_user($userinfo, $c); } -sub prepare { - my $c = shift->NEXT::prepare(@_); +## Consider making this a public method. - would make certain things easier when +## dealing with things pre-auth restore. +sub find_realm_for_persisted_user { + my $c = shift; + + my $realm; + if ($c->can('session') + and $c->config->{'Plugin::Authentication'}{'use_session'} + and $c->session_is_valid + and exists($c->session->{'__user_realm'})) { - if ( $c->isa("Catalyst::Plugin::Session") - and !$c->user ) - { - if ( $c->sessionid and my $frozen_user = $c->session->{__user} ) { - $c->_user($frozen_user); + $realm = $c->auth_realms->{$c->session->{'__user_realm'}}; + if ($realm->user_is_restorable($c)) { + return $realm; + } + } else { + ## we have no choice but to ask each realm whether it has a persisted user. + foreach my $realmname (@{$c->_auth_realm_restore_order}) { + my $realm = $c->auth_realms->{$realmname} + || Catalyst::Exception->throw("Could not find authentication realm '$realmname'"); + return $realm + if $realm->user_is_restorable($c); } } - - return $c; + return undef; } sub auth_restore_user { - my ( $c, $frozen_user, $store_name ) = @_; + my ( $c, $frozen_user, $realmname ) = @_; - return - unless $c->isa("Catalyst::Plugin::Session") - and $c->config->{authentication}{use_session} - and $c->sessionid; + my $realm; + if (defined($realmname)) { + $realm = $c->get_auth_realm($realmname); + } else { + $realm = $c->find_realm_for_persisted_user; + } + return undef unless $realm; # FIXME die unless? This is an internal inconsistency - $store_name ||= $c->session->{__user_store}; - $frozen_user ||= $c->session->{__user}; + $c->_user( my $user = $realm->restore_user( $c, $frozen_user ) ); - my $store = $c->get_auth_store($store_name); - $c->_user( my $user = $store->from_session( $c, $frozen_user ) ); + # this sets the realm the user originated in. + $user->auth_realm($realm->name) if $user; return $user; } +# we can't actually do our setup in setup because the model has not yet been loaded. +# So we have to trigger off of setup_finished. :-( sub setup { - my $c = shift; + my $app = shift; - my $cfg = $c->config->{authentication} || {}; + $app->_authentication_initialize(); + $app->next::method(@_); +} - %$cfg = ( - use_session => 1, - %$cfg, - ); +## the actual initialization routine. whee. +sub _authentication_initialize { + my $app = shift; + + ## let's avoid recreating / configuring everything if we have already done it, eh? + if ($app->can('_auth_realms')) { return }; + + ## make classdata where it is used. + $app->mk_classdata( '_auth_realms' => {}); + + ## the order to attempt restore in - If we don't have session - we have + ## no way to be sure where a frozen user came from - so we have to + ## ask each realm if it can restore the user. Unfortunately it is possible + ## that multiple realms could restore the user from the data we have - + ## So we have to determine at setup time what order to ask the realms in. + ## The default is to use the user_restore_priority values defined in the realm + ## config. if they are not defined - we go by alphabetical order. Note that + ## the 'default' realm always gets first chance at it unless it is explicitly + ## placed elsewhere by user_restore_priority. Remember this only comes + ## into play if session is disabled. + + $app->mk_classdata( '_auth_realm_restore_order' => []); + + my $cfg = $app->config->{'Plugin::Authentication'}; + my $realmshash; + if (!defined($cfg)) { + if (exists($app->config->{'authentication'})) { + $cfg = $app->config->{'authentication'}; + $app->config->{'Plugin::Authentication'} = $app->config->{'authentication'}; + } else { + $cfg = {}; + } + } else { + # the realmshash contains the various configured realms. By default this is + # the main $app->config->{'Plugin::Authentication'} hash - but if that is + # not defined, or there is a subkey {'realms'} then we use that. + $realmshash = $cfg; + } - $c->register_auth_stores( - default => $cfg->{store}, - %{ $cfg->{stores} || {} }, - ); + ## If we have a sub-key of {'realms'} then we use that for realm configuration + if (exists($cfg->{'realms'})) { + $realmshash = $cfg->{'realms'}; + } + + # old default was to force use_session on. This must remain for that + # reason - but if use_session is already in the config, we respect its setting. + if (!exists($cfg->{'use_session'})) { + $cfg->{'use_session'} = 1; + } + + ## if we have a realms hash + if (ref($realmshash) eq 'HASH') { + + my %auth_restore_order; + my $authcount = 2; + my $defaultrealm = 'default'; + + foreach my $realm (sort keys %{$realmshash}) { + if (ref($realmshash->{$realm}) eq 'HASH' && + (exists($realmshash->{$realm}{credential}) || exists($realmshash->{$realm}{class}))) { + + $app->setup_auth_realm($realm, $realmshash->{$realm}); + + if (exists($realmshash->{$realm}{'user_restore_priority'})) { + $auth_restore_order{$realm} = $realmshash->{$realm}{'user_restore_priority'}; + } else { + $auth_restore_order{$realm} = $authcount++; + } + } + } + + # if we have a 'default_realm' in the config hash and we don't already + # have a realm called 'default', we point default at the realm specified + if (exists($cfg->{'default_realm'}) && !$app->get_auth_realm('default')) { + if ($app->_set_default_auth_realm($cfg->{'default_realm'})) { + $defaultrealm = $cfg->{'default_realm'}; + $auth_restore_order{'default'} = $auth_restore_order{$cfg->{'default_realm'}}; + delete($auth_restore_order{$cfg->{'default_realm'}}); + } + } + + ## if the default realm did not have a defined priority in its config - we put it at the front. + if (!exists($realmshash->{$defaultrealm}{'user_restore_priority'})) { + $auth_restore_order{'default'} = 1; + } + + @{$app->_auth_realm_restore_order} = sort { $auth_restore_order{$a} <=> $auth_restore_order{$b} } keys %auth_restore_order; + + } else { + + ## BACKWARDS COMPATIBILITY - if realms is not defined - then we are probably dealing + ## with an old-school config. The only caveat here is that we must add a classname + + ## also - we have to treat {store} as {stores}{default} - because + ## while it is not a clear as a valid config in the docs, it + ## is functional with the old api. Whee! + if (exists($cfg->{'store'}) && !exists($cfg->{'stores'}{'default'})) { + $cfg->{'stores'}{'default'} = $cfg->{'store'}; + } + + push @{$app->_auth_realm_restore_order}, 'default'; + foreach my $storename (keys %{$cfg->{'stores'}}) { + my $realmcfg = { + store => { class => $cfg->{'stores'}{$storename} }, + }; + $app->setup_auth_realm($storename, $realmcfg); + } + } - $c->NEXT::setup(@_); } -sub get_auth_store { - my ( $self, $name ) = @_; - $self->auth_stores->{$name} || ( Class::Inspector->loaded($name) && $name ); +# set up realmname. +sub setup_auth_realm { + my ($app, $realmname, $config) = @_; + + my $realmclass = $config->{class}; + + if( !$realmclass ) { + $realmclass = 'Catalyst::Authentication::Realm'; + } elsif ($realmclass !~ /^\+(.*)$/ ) { + $realmclass = "Catalyst::Authentication::Realm::${realmclass}"; + } else { + $realmclass = $1; + } + + Catalyst::Utils::ensure_class_loaded( $realmclass ); + + my $realm = $realmclass->new($realmname, $config, $app); + if ($realm) { + $app->auth_realms->{$realmname} = $realm; + } else { + $app->log->debug("realm initialization for '$realmname' failed."); + } + return $realm; } -sub get_auth_store_name { - my ( $self, $store ) = @_; - $self->auth_store_names->{$store}; +sub auth_realms { + my $self = shift; + $self->_authentication_initialize(); # Ensure _auth_realms created! + return($self->_auth_realms); +} + +sub get_auth_realm { + my ($app, $realmname) = @_; + return $app->auth_realms->{$realmname}; } -sub register_auth_stores { - my ( $self, %new ) = @_; - foreach my $name ( keys %new ) { - my $store = $new{$name} or next; - $self->auth_stores->{$name} = $store; - $self->auth_store_names->{$store} = $name; +# Very internal method. Vital Valuable Urgent, Do not touch on pain of death. +# Using this method just assigns the default realm to be the value associated +# with the realmname provided. It WILL overwrite any real realm called 'default' +# so can be very confusing if used improperly. It's used properly already. +# Translation: don't use it. +sub _set_default_auth_realm { + my ($app, $realmname) = @_; + + if (exists($app->auth_realms->{$realmname})) { + $app->auth_realms->{'default'} = $app->auth_realms->{$realmname}; } + return $app->get_auth_realm('default'); } -sub auth_stores { - my $self = shift; - $self->_auth_stores(@_) || $self->_auth_stores( {} ); +sub authenticate { + my ($app, $userinfo, $realmname) = @_; + + if (!$realmname) { + $realmname = 'default'; + } + + my $realm = $app->get_auth_realm($realmname); + + ## note to self - make authenticate throw an exception if realm is invalid. + + if ($realm) { + return $realm->authenticate($app, $userinfo); + } else { + Catalyst::Exception->throw( + "authenticate called with nonexistant realm: '$realmname'."); + + } + return undef; } -sub auth_store_names { - my $self = shift; +## BACKWARDS COMPATIBILITY -- Warning: Here be monsters! +# +# What follows are backwards compatibility routines - for use with Stores and Credentials +# that have not been updated to work with C::P::Authentication v0.10. +# These are here so as to not break people's existing installations, but will go away +# in a future version. +# +# The old style of configuration only supports a single store, as each store module +# sets itself as the default store upon being loaded. This is the only supported +# 'compatibility' mode. +# - $self->_auth_store_names || do { - tie my %hash, 'Tie::RefHash'; - $self->_auth_store_names( \%hash ); - } +sub get_user { + my ( $c, $uid, @rest ) = @_; + + return $c->find_user( {'id' => $uid, 'rest'=>\@rest }, 'default' ); } + +## this should only be called when using old-style authentication plugins. IF this gets +## called in a new-style config - it will OVERWRITE the store of your default realm. Don't do it. +## also - this is a partial setup - because no credential is instantiated... in other words it ONLY +## works with old-style auth plugins and C::P::Authentication in compatibility mode. Trying to combine +## this with a realm-type config will probably crash your app. sub default_auth_store { my $self = shift; + my $realm = $self->get_auth_realm('default'); + if (!$realm) { + $realm = $self->setup_auth_realm('default', { class => 'Compatibility' }); + } if ( my $new = shift ) { - $self->register_auth_stores( default => $new ); + $realm->store($new); + + my $storeclass; + if (ref($new)) { + $storeclass = ref($new); + } else { + $storeclass = $new; + } + + # BACKWARDS COMPATIBILITY - if the store class does not define find_user, we define it in terms + # of get_user and add it to the class. this is because the auth routines use find_user, + # and rely on it being present. (this avoids per-call checks) + if (!$storeclass->can('find_user')) { + no strict 'refs'; + *{"${storeclass}::find_user"} = sub { + my ($self, $info) = @_; + my @rest = @{$info->{rest}} if exists($info->{rest}); + $self->get_user($info->{id}, @rest); + }; + } + } + + return $self->get_auth_realm('default')->store; +} + +## BACKWARDS COMPATIBILITY +## this only ever returns a hash containing 'default' - as that is the only +## supported mode of calling this. +sub auth_store_names { + my $self = shift; + + my %hash = ( $self->get_auth_realm('default')->store => 'default' ); +} + +sub get_auth_store { + my ( $self, $name ) = @_; + + if ($name ne 'default') { + Carp::croak "get_auth_store called on non-default realm '$name'. Only default supported in compatibility mode"; + } else { + $self->default_auth_store(); } +} - $self->get_auth_store("default"); +sub get_auth_store_name { + my ( $self, $store ) = @_; + return 'default'; +} + +# sub auth_stores is only used internally - here for completeness +sub auth_stores { + my $self = shift; + + my %hash = ( 'default' => $self->get_auth_realm('default')->store); } __PACKAGE__; @@ -206,34 +489,37 @@ authentication framework. use Catalyst qw/ Authentication - Authentication::Store::Foo - Authentication::Credential::Password /; # later on ... - # ->login is provided by the Credential::Password module - $c->login('myusername', 'mypassword'); - my $age = $c->user->age; + $c->authenticate({ username => 'myusername', + password => 'mypassword' }); + my $age = $c->user->get('age'); $c->logout; =head1 DESCRIPTION -The authentication plugin provides generic user support. It is the basis -for both authentication (checking the user is who they claim to be), and -authorization (allowing the user to do what the system authorises them to do). +The authentication plugin provides generic user support for Catalyst apps. It +is the basis for both authentication (checking the user is who they claim to +be), and authorization (allowing the user to do what the system authorises +them to do). -Using authentication is split into two parts. A Store is used to actually -store the user information, and can store any amount of data related to -the user. Multiple stores can be accessed from within one application. -Credentials are used to verify users, using the store, given data from -the frontend. +Using authentication is split into two parts. A Store is used to actually +store the user information, and can store any amount of data related to the +user. Credentials are used to verify users, using information from the store, +given data from the frontend. A Credential and a Store are paired to form a +'Realm'. A Catalyst application using the authentication framework must have +at least one realm, and may have several. -To implement authentication in a Catalyst application you need to add this -module, plus at least one store and one credential module. +To implement authentication in a Catalyst application you need to add this +module, and specify at least one realm in the configuration. -Authentication data can also be stored in a session, if the application +Authentication data can also be stored in a session, if the application is using the L module. +B in version 0.10 of this module, the interface to this module changed. +Please see L for more information. + =head1 INTRODUCTION =head2 The Authentication/Authorization Process @@ -255,54 +541,80 @@ identity theft for now). Checking the password, or any other proof is called B. By this time you know exactly who the user is - the user's identity is -B. This is where this module's job stops, and other plugins step -in. The next logical step is B, the process of deciding what a -user is (or isn't) allowed to do. For example, say your users are split into -two main groups - regular users and administrators. You should verify that the +B. This is where this module's job stops, and your application +or other plugins step in. + +The next logical step is B, the process of deciding what a user +is (or isn't) allowed to do. For example, say your users are split into two +main groups - regular users and administrators. You want to verify that the currently logged in user is indeed an administrator before performing the -actions of an administrative part of your application. One way to do this is -with role based access control. +actions in an administrative part of your application. These decisions may be +made within your application code using just the information available after +authentication, or it may be facilitated by a number of plugins. =head2 The Components In This Framework +=head3 Realms + +Configuration of the Catalyst::Plugin::Authentication framework is done in +terms of realms. In simplest terms, a realm is a pairing of a Credential +verifier and a User storage (Store) backend. As of version 0.10003, realms are +now objects that you can create and customize. + +An application can have any number of Realms, each of which operates +independent of the others. Each realm has a name, which is used to identify it +as the target of an authentication request. This name can be anything, such as +'users' or 'members'. One realm must be defined as the default_realm, which is +used when no realm name is specified. More information about configuring +realms is available in the configuration section. + =head3 Credential Verifiers -When user input is transferred to the L application (typically via -form inputs) this data then enters the authentication framework through these -plugins. +When user input is transferred to the L application +(typically via form inputs) the application may pass this information +into the authentication system through the C<< $c->authenticate() >> +method. From there, it is passed to the appropriate Credential +verifier. These plugins check the data, and ensure that it really proves the user is who they claim to be. +Credential verifiers compatible with versions of this module 0.10x and +upwards should be in the namespace +C. + =head3 Storage Backends -The credentials also identify a user, and this family of modules is supposed to -take this identification data and return a standardized object oriented -representation of users. +The authentication data also identifies a user, and the Storage backend modules +use this data to locate and return a standardized object-oriented +representation of a user. When a user is retrieved from a store it is not necessarily authenticated. -Credential verifiers can either accept a user object, or fetch the object -themselves from the default store. +Credential verifiers accept a set of authentication data and use this +information to retrieve the user from the store they are paired with. + +Storage backends compatible with versions of this module 0.10x and +upwards should be in the namespace +C. =head3 The Core Plugin -This plugin on its own is the glue, providing store registration, session +This plugin on its own is the glue, providing realm configuration, session integration, and other goodness for the other plugins. =head3 Other Plugins More layers of plugins can be stacked on top of the authentication code. For example, L provides an abstraction of -browser sessions that is more persistent per users. +browser sessions that is more persistent per user. L provides an accepted way to separate and group users into categories, and then check which categories the current user belongs to. =head1 EXAMPLE -Let's say we were storing users in an Apache style htpasswd file. Users are -stored in that file, with a hashed password and some extra comments. Users are -verified by supplying a password which is matched with the file. +Let's say we were storing users in a simple Perl hash. Users are +verified by supplying a password which is matched within the hash. This means that our application will begin like this: @@ -310,30 +622,51 @@ This means that our application will begin like this: use Catalyst qw/ Authentication - Authentication::Credential::Password - Authentication::Store::Htpasswd /; - __PACKAGE__->config->{authentication}{htpasswd} = "passwdfile"; + __PACKAGE__->config( 'Plugin::Authentication' => + { + default => { + credential => { + class => 'Password', + password_field => 'password', + password_type => 'clear' + }, + store => { + class => 'Minimal', + users => { + bob => { + password => "s00p3r", + editor => 'yes', + roles => [qw/edit delete/], + }, + william => { + password => "s3cr3t", + roles => [qw/comment/], + } + } + } + } + } + ); -This loads the appropriate methods and also sets the htpasswd store as the -default store. - -So, now that we have the code loaded we need to get data from the user into the -credential verifier. +This tells the authentication plugin what realms are available, which +credential and store modules are used, and the configuration of each. With +this code loaded, we can now attempt to authenticate users. -Let's create an authentication controller: +To show an example of this, let's create an authentication controller: package MyApp::Controller::Auth; sub login : Local { my ( $self, $c ) = @_; - if ( my $user = $c->req->param("user") - and my $password = $c->req->param("password") ) + if ( my $user = $c->req->params->{user} + and my $password = $c->req->params->{password} ) { - if ( $c->login( $user, $password ) ) { - $c->res->body( "hello " . $c->user->name ); + if ( $c->authenticate( { username => $user, + password => $password } ) ) { + $c->res->body( "hello " . $c->user->get("name") ); } else { # login incorrect } @@ -343,61 +676,62 @@ Let's create an authentication controller: } } -This code should be very readable. If all the necessary fields are supplied, -call the L method on the -controller. If that succeeds the user is logged in. +This code should be self-explanatory. If all the necessary fields are supplied, +call the C method on the context object. If it succeeds the +user is logged in. -It could be simplified though: +The credential verifier will attempt to retrieve the user whose +details match the authentication information provided to +C<< $c->authenticate() >>. Once it fetches the user the password is +checked and if it matches the user will be B and +C<< $c->user >> will contain the user object retrieved from the store. - sub login : Local { - my ( $self, $c ) = @_; +In the above case, the default realm is checked, but we could just as easily +check an alternate realm. If this were an admin login, for example, we could +authenticate on the admin realm by simply changing the C<< $c->authenticate() >> +call: - if ( $c->login ) { - ... - } - } + if ( $c->authenticate( { username => $user, + password => $password }, 'admin' ) ) { + $c->res->body( "hello " . $c->user->get("name") ); + } ... -Since the C method knows how to find logically named parameters on it's -own. -The credential verifier will ask the default store to get the user whose ID is -the user parameter. In this case the default store is the htpasswd one. Once it -fetches the user from the store the password is checked and if it's OK -C<< $c->user >> will contain the user object returned from the htpasswd store. +Now suppose we want to restrict the ability to edit to a user with an +'editor' value of yes. -We can also pass a user object to the credential verifier manually, if we have -several stores per app. This is discussed in -L. +The restricted action might look like this: -Now imagine each admin user has a comment set in the htpasswd file saying -"admin". - -A restricted action might look like this: - - sub restricted : Local { + sub edit : Local { my ( $self, $c ) = @_; $c->detach("unauthorized") unless $c->user_exists - and $c->user->extra_info() eq "admin"; + and $c->user->get('editor') eq 'yes'; # do something restricted here } -This is somewhat similar to role based access control. -L treats the extra info -field as a comma separated list of roles if it's treated that way. Let's -leverage this. Add the role authorization plugin: +(Note that if you have multiple realms, you can use +C<< $c->user_in_realm('realmname') >> in place of +C<< $c->user_exists(); >> This will essentially perform the same +verification as user_exists, with the added requirement that if there +is a user, it must have come from the realm specified.) + +The above example is somewhat similar to role based access control. +L treats the roles field as +an array of role names. Let's leverage this. Add the role authorization +plugin: use Catalyst qw/ ... Authorization::Roles /; - sub restricted : Local { + sub edit : Local { my ( $self, $c ) = @_; - $c->detach("unauthorized") unless $c->check_roles("admin"); + $c->detach("unauthorized") unless $c->check_user_roles("edit"); # do something restricted here } @@ -405,200 +739,266 @@ leverage this. Add the role authorization plugin: This is somewhat simpler and will work if you change your store, too, since the role interface is consistent. -Let's say your app grew, and you now have 10000 users. It's no longer efficient -to maintain an htpasswd file, so you move this data to a database. - - use Catalyst qw/ - Authentication - Authentication::Credential::Password - Authentication::Store::DBIC - Authorization::Roles - /; +Let's say your app grows, and you now have 10,000 users. It's no longer +efficient to maintain a hash of users, so you move this data to a database. +You can accomplish this simply by installing the L Store and +changing your config: + + __PACKAGE__->config( 'Plugin::Authentication' => + { + default_realm => 'members', + members => { + credential => { + class => 'Password', + password_field => 'password', + password_type => 'clear' + }, + store => { + class => 'DBIx::Class', + user_model => 'MyApp::Users', + role_column => 'roles', + } + } + } + ); - __PACKAGE__->config->{authentication}{dbic} = ...; # see the DBIC store docs +The authentication system works behind the scenes to load your data from the +new source. The rest of your application is completely unchanged. -The rest of your code should be unchanged. Now let's say you are integrating -typekey authentication to your system. For simplicity's sake we'll assume that -the user's are still keyed in the same way. - use Catalyst qw/ - Authentication - Authentication::Credential::Password - Authentication::Credential::TypeKey - Authentication::Store::DBIC - Authorization::Roles - /; +=head1 CONFIGURATION -And in your auth controller add a new action: + # example + __PACKAGE__->config( 'Plugin::Authentication' => + { + default_realm => 'members', + + members => { + credential => { + class => 'Password', + password_field => 'password', + password_type => 'clear' + }, + store => { + class => 'DBIx::Class', + user_model => 'MyApp::Users', + role_column => 'roles', + } + }, + admins => { + credential => { + class => 'Password', + password_field => 'password', + password_type => 'clear' + }, + store => { + class => '+MyApp::Authentication::Store::NetAuth', + authserver => '192.168.10.17' + } + } + } + ); - sub typekey : Local { - my ( $self, $c ) = @_; +NOTE: Until version 0.10008 of this module, you would need to put all the +realms inside a "realms" key in the configuration. Please see +L for more information - if ( $c->authenticate_typekey) { # uses $c->req and Authen::TypeKey - # same stuff as the $c->login method - # ... - } - } +=over 4 -You've now added a new credential verification mechanizm orthogonally to the -other components. All you have to do is make sure that the credential verifiers -pass on the same types of parameters to the store in order to retrieve user -objects. +=item use_session -=head1 METHODS +Whether or not to store the user's logged in state in the session, if the +application is also using L. This +value is set to true per default. -=over 4 +However, even if use_session is disabled, if any code touches $c->session, a session +object will be auto-vivified and session Cookies will be sent in the headers. To +prevent accidental session creation, check if a session already exists with +if ($c->sessionid) { ... }. If the session doesn't exist, then don't place +anything in the session to prevent an unecessary session from being created. + +=item default_realm + +This defines which realm should be used as when no realm is provided to methods +that require a realm such as authenticate or find_user. + +=item realm refs + +The Plugin::Authentication config hash contains the series of realm +configurations you want to use for your app. The only rule here is +that there must be at least one. A realm consists of a name, which is used +to reference the realm, a credential and a store. You may also put your +realm configurations within a subelement called 'realms' if you desire to +separate them from the remainder of your configuration. Note that if you use +a 'realms' subelement, you must put ALL of your realms within it. + +You can also specify a realm class to instantiate instead of the default +L class using the 'class' element within the +realm config. + +Each realm config contains two hashes, one called 'credential' and one called +'store', each of which provide configuration details to the respective modules. +The contents of these hashes is specific to the module being used, with the +exception of the 'class' element, which tells the core Authentication module the +classname to instantiate. + +The 'class' element follows the standard Catalyst mechanism of class +specification. If a class is prefixed with a +, it is assumed to be a complete +class name. Otherwise it is considered to be a portion of the class name. For +credentials, the classname 'B', for example, is expanded to +Catalyst::Authentication::Credential::B. For stores, the +classname 'B' is expanded to: +Catalyst::Authentication::Store::B. -=item user +=back -Returns the currently logged in user or undef if there is none. +=head1 METHODS -=item user_exists +=head2 $c->authenticate( $userinfo [, $realm ]) -Whether or not a user is logged in right now. +Attempts to authenticate the user using the information in the $userinfo hash +reference using the realm $realm. $realm may be omitted, in which case the +default realm is checked. -The reason this method exists is that C<< $c->user >> may needlessly load the -user from the auth store. +=head2 $c->user( ) -If you're just going to say +Returns the currently logged in user, or undef if there is none. +Normally the user is re-retrieved from the store. +For L the user is re-restored +using the primary key of the user table. +Thus B can throw an error even though B +returned true. - if ( $c->user_exists ) { - # foo - } else { - $c->forward("login"); - } +=head2 $c->user_exists( ) -it should be more efficient than C<< $c->user >> when a user is marked in the -session but C<< $c->user >> hasn't been called yet. +Returns true if a user is logged in right now. The difference between +B and B is that user_exists will return true if a user is logged +in, even if it has not been yet retrieved from the storage backend. If you only +need to know if the user is logged in, depending on the storage mechanism this +can be much more efficient. +B only looks into the session while B is trying to restore the user. -=item logout +=head2 $c->user_in_realm( $realm ) -Delete the currently logged in user from C and the session. +Works like user_exists, except that it only returns true if a user is both +logged in right now and was retrieved from the realm provided. -=item get_user $uid +=head2 $c->logout( ) -Fetch a particular users details, defined by the given ID, via the default store. +Logs the user out. Deletes the currently logged in user from C<< $c->user >> +and the session. It does not delete the session. -=back +=head2 $c->find_user( $userinfo, $realm ) -=head1 CONFIGURATION +Fetch a particular users details, matching the provided user info, from the realm +specified in $realm. -=over 4 + $user = $c->find_user({ id => $id }); + $c->set_authenticated($user); # logs the user in and calls persist_user -=item use_session +=head2 persist_user() -Whether or not to store the user's logged in state in the session, if the -application is also using the L plugin. This -value is set to true per default. +Under normal circumstances the user data is only saved to the session during +initial authentication. This call causes the auth system to save the +currently authenticated user's data across requests. Useful if you have +changed the user data and want to ensure that future requests reflect the +most current data. Assumes that at the time of this call, $c->user +contains the most current data. -=item store +=head2 find_realm_for_persisted_user() -If multiple stores are being used, set the module you want as default here. +Private method, do not call from user code! -=item stores +=head1 INTERNAL METHODS -If multiple stores are being used, you need to provide a name for each store -here, as a hash, the keys are the names you wish to use, and the values are -the the names of the plugins. +These methods are for Catalyst::Plugin::Authentication B only. +Please do not use them in your own code, whether application or credential / +store modules. If you do, you will very likely get the nasty shock of having +to fix / rewrite your code when things change. They are documented here only +for reference. - # example - __PACKAGE__->config( authentication => { - store => 'Catalyst::Plugin::Authentication::Store::HtPasswd', - stores => { - 'dbic' => 'Catalyst::Plugin::Authentication::Store::DBIC' - } - }); +=head2 $c->set_authenticated( $user, $realmname ) -=back +Marks a user as authenticated. This is called from within the authenticate +routine when a credential returns a user. $realmname defaults to 'default'. +You can use find_user to get $user -=head1 METHODS FOR STORE MANAGEMENT +=head2 $c->auth_restore_user( $user, $realmname ) -=over 4 +Used to restore a user from the session. In most cases this is called without +arguments to restore the user via the session. Can be called with arguments +when restoring a user from some other method. Currently not used in this way. -=item default_auth_store +=head2 $c->auth_realms( ) -Return the store whose name is 'default'. +Returns a hashref containing realmname -> realm instance pairs. Realm +instances contain an instantiated store and credential object as the 'store' +and 'credential' elements, respectively -This is set to C<< $c->config->{authentication}{store} >> if that value exists, -or by using a Store plugin: +=head2 $c->get_auth_realm( $realmname ) - use Catalyst qw/Authentication Authentication::Store::Minimal/; +Retrieves the realm instance for the realmname provided. -Sets the default store to -L. +=head2 $c->update_user_in_session +This was a short-lived method to update user information - you should use persist_user instead. -=item get_auth_store $name +=head2 $c->setup_auth_realm( ) -Return the store whose name is $name. +=head1 OVERRIDDEN METHODS -=item get_auth_store_name $store +=head2 $c->setup( ) -Return the name of the store $store. - -=item auth_stores +=head1 SEE ALSO -A hash keyed by name, with the stores registered in the app. +This list might not be up to date. Below are modules known to work with the updated +API of 0.10 and are therefore compatible with realms. -=item auth_store_names +=head2 Realms -A ref-hash keyed by store, which contains the names of the stores. +L -=item register_auth_stores %stores_by_name +=head2 User Storage Backends -Register stores into the application. +=over -=back +=item L -=head1 INTERNAL METHODS +=item L -=over 4 +=item L -=item set_authenticated $user +=item L -Marks a user as authenticated. Should be called from a -C plugin after successful -authentication. +=item L -This involves setting C and the internal data in C if -L is loaded. +=item L -=item auth_restore_user $user +=item L -Used to restore a user from the session, by C only when it's actually -needed. +=back -=item save_user_in_session $user +=head2 Credential verification -Used to save the user in a session. +=over -=item prepare +=item L -Revives a user from the session object if there is one. +=item L -=item setup +=item L -Sets the default configuration parameters. +=item L -=item +=item L -=back +=item L -=head1 SEE ALSO - -This list might not be up to date. - -=head2 User Storage Backends +=item L -L, -L, -L (also works with Class::DBI). +=item L -=head2 Credential verification - -L, -L, -L +=back =head2 Authorization @@ -607,7 +1007,7 @@ L =head2 Internals Documentation -L +L =head2 Misc @@ -620,25 +1020,128 @@ This module along with its sub plugins deprecate a great number of other modules. These include L, L. -At the time of writing these plugins have not yet been replaced or updated, but -should be eventually: L, -L, -L, -L. +=head1 INCOMPATABILITIES + +The realms-based configuration and functionality of the 0.10 update +of L required a change in the API used by +credentials and stores. It has a compatibility mode which allows use of +modules that have not yet been updated. This, however, completely mimics the +older api and disables the new realm-based features. In other words you cannot +mix the older credential and store modules with realms, or realm-based +configs. The changes required to update modules are relatively minor and are +covered in L. We hope that most +modules will move to the compatible list above very quickly. + +=head1 COMPATIBILITY CONFIGURATION + +Until version 0.10008 of this module, you needed to put all the +realms inside a "realms" key in the configuration. + + # example + __PACKAGE__->config( 'Plugin::Authentication' => + { + default_realm => 'members', + realms => { + members => { + ... + }, + }, + } + ); + +If you use the old, deprecated C<< __PACKAGE__->config( 'authentication' ) >> +configuration key, then the realms key is still required. + +=head1 COMPATIBILITY ROUTINES + +In version 0.10 of L, the API +changed. For app developers, this change is fairly minor, but for +Credential and Store authors, the changes are significant. + +Please see the documentation in version 0.09 of +Catalyst::Plugin::Authentication for a better understanding of how the old API +functioned. + +The items below are still present in the plugin, though using them is +deprecated. They remain only as a transition tool, for those sites which can +not yet be upgraded to use the new system due to local customizations or use +of Credential / Store modules that have not yet been updated to work with the +new API. + +These routines should not be used in any application using realms +functionality or any of the methods described above. These are for reference +purposes only. + +=head2 $c->login( ) + +This method is used to initiate authentication and user retrieval. Technically +this is part of the old Password credential module and it still resides in the +L class. It is +included here for reference only. + +=head2 $c->default_auth_store( ) + +Return the store whose name is 'default'. + +This is set to C<< $c->config( 'Plugin::Authentication' => { store => # Store} ) >> if that value exists, +or by using a Store plugin: + + # load the Minimal authentication store. + use Catalyst qw/Authentication Authentication::Store::Minimal/; + +Sets the default store to +L. + +=head2 $c->get_auth_store( $name ) + +Return the store whose name is $name. + +=head2 $c->get_auth_store_name( $store ) + +Return the name of the store $store. + +=head2 $c->auth_stores( ) + +A hash keyed by name, with the stores registered in the app. + +=head2 $c->register_auth_stores( %stores_by_name ) + +Register stores into the application. + +=head2 $c->auth_store_names( ) + +=head2 $c->get_user( ) =head1 AUTHORS Yuval Kogman, C +Jay Kuri, C + Jess Robinson David Kamholz +Tomas Doran (t0m), C + +kmx + +Nigel Metheringham + +Florian Ragwitz C + +Stephan Jauernick C + +Oskari Ojala (Okko), C + =head1 COPYRIGHT & LICENSE - Copyright (c) 2005 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. +Copyright (c) 2005 - 2011 +the Catalyst::Plugin::Authentication L +as listed above. + +This program is free software; you can redistribute +it and/or modify it under the same terms as Perl itself. =cut