From: Matt S Trout Date: Tue, 17 Jul 2007 16:57:33 +0000 (+0000) Subject: Updates to authentication system. Initial import of modifications. X-Git-Tag: v0.10009_01~85 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=54c8dc06a4c1aec61a6595f0d19c42c8d0eed749;p=catagits%2FCatalyst-Plugin-Authentication.git Updates to authentication system. Initial import of modifications. POD documentation NOT complete yet... so this is a RTFS import r33939@cain (orig r5489): jayk | 2006-11-10 22:29:07 +0000 --- diff --git a/lib/Catalyst/Plugin/Authentication.pm b/lib/Catalyst/Plugin/Authentication.pm index 2dc02ff..7a50835 100644 --- a/lib/Catalyst/Plugin/Authentication.pm +++ b/lib/Catalyst/Plugin/Authentication.pm @@ -6,7 +6,7 @@ use base qw/Class::Accessor::Fast Class::Data::Inheritable/; BEGIN { __PACKAGE__->mk_accessors(qw/_user/); - __PACKAGE__->mk_classdata($_) for qw/_auth_stores _auth_store_names/; + __PACKAGE__->mk_classdata($_) for qw/_auth_realms/; } use strict; @@ -22,19 +22,27 @@ use Class::Inspector; # constant->import(have_want => eval { require Want }); #} -our $VERSION = "0.09"; +our $VERSION = "0.10"; sub set_authenticated { - my ( $c, $user ) = @_; + my ( $c, $user, $realmname ) = @_; $c->user($user); $c->request->{user} = $user; # compatibility kludge - if ( $c->_should_save_user_in_session($user) ) { - $c->save_user_in_session($user); + if (!$realmname) { + $realmname = 'default'; } - - $c->NEXT::set_authenticated($user); + + if ( $c->isa("Catalyst::Plugin::Session") + and $c->config->{authentication}{use_session} + and $user->supports("session") ) + { + $c->save_user_in_session($realmname, $user); + } + $user->_set_auth_realm($realmname); + + $c->NEXT::set_authenticated($user, $realmname); } sub _should_save_user_in_session { @@ -72,17 +80,28 @@ sub 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->_user_in_session); } + sub save_user_in_session { - my ( $c, $user ) = @_; + my ( $c, $realmname, $user ) = @_; - my $store = $user->store || ref $user; - $c->session->{__user_store} = $c->get_auth_store_name($store) || $store; - $c->session->{__user} = $user->for_session; + $c->session->{__user_realm} = $realmname; + + # we want to ask the backend for a user prepared for the session. + # but older modules split this functionality between the user and the + # backend. 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 logout { @@ -90,31 +109,30 @@ sub logout { $c->user(undef); - if ( $c->_should_load_user_from_session ) { - $c->_delete_user_from_session(); + if ( + $c->isa("Catalyst::Plugin::Session") + and $c->config->{authentication}{use_session} + and $c->session_is_valid + ) { + delete @{ $c->session }{qw/__user __user_realm/}; } $c->NEXT::logout(@_); } -sub _delete_user_from_session { - my $c = shift; - delete @{ $c->session }{qw/__user __user_store/}; -} - -sub get_user { - my ( $c, $uid, @rest ) = @_; - - if ( my $store = $c->default_auth_store ) { - return $store->get_user( $uid, @rest ); - } - else { - Catalyst::Exception->throw( - "The user id $uid was passed to an authentication " - . "plugin, but no default store was specified" ); +sub find_user { + my ( $c, $userinfo, $realmname ) = @_; + + $realmname ||= 'default'; + my $realm = $c->get_auth_realm($realmname); + if ( $realm->{'store'} ) { + return $realm->{'store'}->find_user($userinfo, $c); + } else { + $c->log->debug('find_user: unable to locate a store matching the requested realm'); } } + sub _user_in_session { my $c = shift; @@ -132,84 +150,260 @@ sub _store_in_session { } sub auth_restore_user { - my ( $c, $frozen_user, $store_name ) = @_; + my ( $c, $frozen_user, $realmname ) = @_; $frozen_user ||= $c->_user_in_session; return unless defined($frozen_user); - $store_name ||= $c->_store_in_session; - return unless $store_name; # FIXME die unless? This is an internal inconsistency - - my $store = $c->get_auth_store($store_name); - - $c->_user( my $user = $store->from_session( $c, $frozen_user ) ); + $realmname ||= $c->session->{__user_realm}; + return unless $realmname; # FIXME die unless? This is an internal inconsistency + my $realm = $c->get_auth_realm($realmname); + $c->_user( my $user = $realm->{'store'}->from_session( $c, $frozen_user ) ); + + # this sets the realm the user originated in. + $user->_set_auth_realm($realmname); 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 $cfg = $c->config->{authentication} ||= {}; + $c->_authentication_initialize(); + $c->NEXT::setup(@_); +} + +## the actual initialization routine. whee. +sub _authentication_initialize { + my $c = shift; + + if ($c->_auth_realms) { return }; + + my $cfg = $c->config->{'authentication'} || {}; %$cfg = ( use_session => 1, %$cfg, ); - $c->register_auth_stores( - default => $cfg->{store}, - %{ $cfg->{stores} || {} }, - ); + my $realmhash = {}; + $c->_auth_realms($realmhash); + + ## BACKWARDS COMPATIBILITY - if realm is not defined - then we are probably dealing + ## with an old-school config. The only caveat here is that we must add a classname + if (exists($cfg->{'realms'})) { + + foreach my $realm (keys %{$cfg->{'realms'}}) { + $c->setup_auth_realm($realm, $cfg->{'realms'}{$realm}); + } - $c->NEXT::setup(@_); + # 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'}) && !$c->get_auth_realm('default')) { + $c->set_default_auth_realm($cfg->{'default_realm'}); + } + } else { + foreach my $storename (keys %{$cfg->{'stores'}}) { + my $realmcfg = { + store => $cfg->{'stores'}{$storename}, + }; + $c->setup_auth_realm($storename, $realmcfg); + } + } + } -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) = @_; + + $app->log->debug("Setting up $realmname"); + if (!exists($config->{'store'}{'class'})) { + Carp::croak "Couldn't setup the authentication realm named '$realmname', no class defined"; + } + + # use the + my $storeclass = $config->{'store'}{'class'}; + + ## follow catalyst class naming - a + prefix means a fully qualified class, otherwise it's + ## taken to mean C::P::A::Store::(specifiedclass)::Backend + if ($storeclass !~ /^\+(.*)$/ ) { + $storeclass = "Catalyst::Plugin::Authentication::Store::${storeclass}::Backend"; + } else { + $storeclass = $1; + } + + + # a little niceness - since most systems seem to use the password credential class, + # if no credential class is specified we use password. + $config->{credential}{class} ||= "Catalyst::Plugin::Authentication::Credential::Password"; + + my $credentialclass = $config->{'credential'}{'class'}; + + ## follow catalyst class naming - a + prefix means a fully qualified class, otherwise it's + ## taken to mean C::P::A::Credential::(specifiedclass) + if ($credentialclass !~ /^\+(.*)$/ ) { + $credentialclass = "Catalyst::Plugin::Authentication::Credential::${credentialclass}"; + } else { + $credentialclass = $1; + } + + # if we made it here - we have what we need to load the classes; + Catalyst::Utils::ensure_class_loaded( $credentialclass ); + Catalyst::Utils::ensure_class_loaded( $storeclass ); + + # 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); + }; + } + + $app->auth_realms->{$realmname}{'store'} = $storeclass->new($config->{'store'}, $app); + if ($credentialclass->can('new')) { + $app->auth_realms->{$realmname}{'credential'} = $credentialclass->new($config->{'credential'}, $app); + } else { + # if the credential class is not actually a class - has no 'new' operator, we wrap it, + # once again - to allow our code to be simple at runtime and allow non-OO packages to function. + my $wrapperclass = 'Catalyst::Plugin::Authentication::Credential::Wrapper'; + Catalyst::Utils::ensure_class_loaded( $wrapperclass ); + $app->auth_realms->{$realmname}{'credential'} = $wrapperclass->new($config->{'credential'}, $app); + } } -sub get_auth_store_name { - my ( $self, $store ) = @_; - $self->auth_store_names->{$store}; +sub auth_realms { + my $self = shift; + return($self->_auth_realms); } -sub register_auth_stores { - my ( $self, %new ) = @_; +sub get_auth_realm { + my ($app, $realmname) = @_; + return $app->auth_realms->{$realmname}; +} - foreach my $name ( keys %new ) { - my $store = $new{$name} or next; - $self->auth_stores->{$name} = $store; - $self->auth_store_names->{$store} = $name; +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); + + if ($realm && exists($realm->{'credential'})) { + my $user = $realm->{'credential'}->authenticate($app, $realm->{store}, $userinfo); + if ($user) { + $app->set_authenticated($user, $realmname); + return $user; + } + } else { + $app->log->debug("The realm requested, '$realmname' does not exist," . + " or there is no credential associated with it.") + } + return 0; } -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; if ( my $new = shift ) { - $self->register_auth_stores( default => $new ); + $self->auth_realms->{'default'}{'store'} = $new; + my $storeclass = ref($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); + }; + } } - $self->get_auth_store("default"); + 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(); + } +} + +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__; __END__ @@ -225,14 +419,11 @@ 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 diff --git a/lib/Catalyst/Plugin/Authentication/Credential/Password.pm b/lib/Catalyst/Plugin/Authentication/Credential/Password.pm index f9d9b62..d98b70c 100644 --- a/lib/Catalyst/Plugin/Authentication/Credential/Password.pm +++ b/lib/Catalyst/Plugin/Authentication/Credential/Password.pm @@ -9,36 +9,105 @@ use Scalar::Util (); use Catalyst::Exception (); use Digest (); -sub login { - my ( $c, $user, $password, @rest ) = @_; +sub new { + my ($class, $config, $app) = @_; + + my $self = { %{$config} }; + $self->{'password_field'} ||= 'password'; + $self->{'password_type'} ||= 'clear'; + $self->{'password_hash_type'} ||= 'SHA-1'; + + if (!grep /$$self{'password_type'}/, ('clear', 'hashed', 'salted_hash', 'crypted', 'self_check')) { + Catalyst::Exception->throw(__PACKAGE__ . " used with unsupported password type: " . $self->{'password_type'}); + } - for ( $c->request ) { - unless ( - defined($user) - or - $user = $_->param("login") - || $_->param("user") - || $_->param("username") - ) { - $c->log->debug( - "Can't login a user without a user object or user ID param") - if $c->debug; - return; + bless $self, $class; +} + +sub authenticate { + my ( $self, $c, $authstore, $authinfo ) = @_; + + my $user_obj = $authstore->find_user($authinfo, $c); + if ($user_obj) { + if ($self->check_password($user_obj, $authinfo)) { + return $user_obj; } + } else { + $c->log->debug("Unable to locate user matching user info provided"); + return; + } +} - unless ( - defined($password) - or - $password = $_->param("password") - || $_->param("passwd") - || $_->param("pass") - ) { - $c->log->debug("Can't login a user without a password") - if $c->debug; - return; +sub check_password { + my ( $self, $user, $authinfo ) = @_; + + if ($self->{'password_type'} eq 'self_check') { + return $user->check_password($authinfo->{$self->{'password_field'}}); + } else { + my $password = $authinfo->{$self->{'password_field'}}; + my $storedpassword = $user->get($self->{'password_field'}); + + if ($self->{password_type} eq 'clear') { + return $password eq $storedpassword; + } elsif ($self->{'password_type'} eq 'crypted') { + return $storedpassword eq crypt( $password, $storedpassword ); + } elsif ($self->{'password_type'} eq 'salted_hash') { + require Crypt::SaltedHash; + my $salt_len = $self->{'password_salt_len'} ? $self->{'password_salt_len'} : 0; + return Crypt::SaltedHash->validate( $storedpassword, $password, + $salt_len ); + } elsif ($self->{'password_type'} eq 'hashed') { + + my $d = Digest->new( $self->{'password_hash_type'} ); + $d->add( $self->{'password_pre_salt'} || '' ); + $d->add($password); + $d->add( $self->{'password_post_salt'} || '' ); + + my $computed = $d->clone()->digest; + my $b64computed = $d->clone()->b64digest; + return ( ( $computed eq $storedpassword ) + || ( unpack( "H*", $computed ) eq $storedpassword ) + || ( $b64computed eq $storedpassword) + || ( $b64computed.'=' eq $storedpassword) ); } } +} +## BACKWARDS COMPATIBILITY - all subs below here are deprecated +## They are here for compatibility with older modules that use / inherit from C::P::A::Password +## login()'s existance relies rather heavily on the fact that Credential::Password +## is being used as a credential. This may not be the case. This is only here +## for backward compatibility. It will go away in a future version +## login should not be used in new applications. + +sub login { + my ( $c, $user, $password, @rest ) = @_; + + unless ( + defined($user) + or + $user = $c->request->param("login") + || $c->request->param("user") + || $c->request->param("username") + ) { + $c->log->debug( + "Can't login a user without a user object or user ID param") + if $c->debug; + return; + } + + unless ( + defined($password) + or + $password = $c->request->param("password") + || $c->request->param("passwd") + || $c->request->param("pass") + ) { + $c->log->debug("Can't login a user without a password") + if $c->debug; + return; + } + unless ( Scalar::Util::blessed($user) and $user->isa("Catalyst::Plugin::Authentication::User") ) { @@ -64,11 +133,13 @@ sub login { if $c->debug; return; } + } +## also deprecated. Here for compatibility with older credentials which do not inherit from C::P::A::Password sub _check_password { my ( $c, $user, $password ) = @_; - + if ( $user->supports(qw/password clear/) ) { return $user->password eq $password; } diff --git a/lib/Catalyst/Plugin/Authentication/Store/Minimal.pm b/lib/Catalyst/Plugin/Authentication/Store/Minimal.pm index aeee176..cecdfa7 100644 --- a/lib/Catalyst/Plugin/Authentication/Store/Minimal.pm +++ b/lib/Catalyst/Plugin/Authentication/Store/Minimal.pm @@ -11,8 +11,8 @@ sub setup { my $c = shift; $c->default_auth_store( - Catalyst::Plugin::Authentication::Store::Minimal::Backend->new( - $c->config->{authentication}{users} + Catalyst::Plugin::Authentication::Store::Minimal::Backend->new( + $c->config->{authentication}{users}, $c ) ); diff --git a/lib/Catalyst/Plugin/Authentication/Store/Minimal/Backend.pm b/lib/Catalyst/Plugin/Authentication/Store/Minimal/Backend.pm index d08e330..2cf5c1f 100644 --- a/lib/Catalyst/Plugin/Authentication/Store/Minimal/Backend.pm +++ b/lib/Catalyst/Plugin/Authentication/Store/Minimal/Backend.pm @@ -9,9 +9,9 @@ use Catalyst::Plugin::Authentication::User::Hash; use Scalar::Util (); sub new { - my ( $class, $hash ) = @_; + my ( $class, $config, $app) = @_; - bless { hash => $hash }, $class; + bless { hash => $config }, $class; } sub from_session { @@ -19,25 +19,28 @@ sub from_session { return $id if ref $id; - $self->get_user( $id ); + $self->find_user( { id => $id } ); } -sub get_user { - my ( $self, $id ) = @_; +## this is not necessarily a good example of what find_user can do, since all we do is +## look up with the id anyway. find_user can be used to locate a user based on other +## combinations of data. See C::P::Authentication::Store::DBIx::Class for a better example +sub find_user { + my ( $self, $userinfo, $c ) = @_; - return unless exists $self->{hash}{$id}; + my $id = $userinfo->{'id'}; + + return unless exists $self->{'hash'}{$id}; - my $user = $self->{hash}{$id}; + my $user = $self->{'hash'}{$id}; if ( ref $user ) { if ( Scalar::Util::blessed($user) ) { - $user->store( $self ); $user->id( $id ); return $user; } elsif ( ref $user eq "HASH" ) { $user->{id} ||= $id; - $user->{store} ||= $self; return bless $user, "Catalyst::Plugin::Authentication::User::Hash"; } else { @@ -64,6 +67,18 @@ sub user_supports { $user->supports(@_); } +## Backwards compatibility +# +# This is a backwards compatible routine. get_user is specifically for loading a user by it's unique id +# find_user is capable of doing the same by simply passing { id => $id } +# no new code should be written using get_user as it is deprecated. +sub get_user { + my ( $self, $id ) = @_; + $self->find_user({id => $id}); +} + + + __PACKAGE__; __END__ diff --git a/lib/Catalyst/Plugin/Authentication/User.pm b/lib/Catalyst/Plugin/Authentication/User.pm index 9fb90eb..5961220 100644 --- a/lib/Catalyst/Plugin/Authentication/User.pm +++ b/lib/Catalyst/Plugin/Authentication/User.pm @@ -5,9 +5,17 @@ package Catalyst::Plugin::Authentication::User; use strict; use warnings; -sub id { die "virtual" } -sub store { die "virtual" } +## chances are you want to override this. +sub id { shift->get('id'); } + +## returns the realm the user came from - not a good idea to override this. +sub auth_realm { + my $self = shift; + $self->{'realm'}; +} + + sub supports { my ( $self, @spec ) = @_; @@ -25,6 +33,47 @@ sub supports { return $cursor; } +## REQUIRED. +## get should return the value of the field specified as it's single argument from the underlying +## user object. This is here to provide a simple, standard way of accessing individual elements of a user +## object - ensuring no overlap between C::P::A::User methods and actual fieldnames. +## this is not the most effecient method, since it uses introspection. If you have an underlying object +## you most likely want to write this yourself. +sub get { + my ($self, $field) = @_; + + my $object; + if ($object = $self->get_object && $object->can($field)) { + return $object->$field(); + } else { + return undef; + } +} + +## REQUIRED. +## get_object should return the underlying user object. This is for when more advanced uses of the +## user is required. Modifications to the existing user, etc. Changes in the object returned +## by this routine may not be reflected in the C::P::A::User object - if this is required, re-authenticating +## the user is probably the best route to take. +## note that it is perfectly acceptable to return $self in cases where there is no underlying object. +sub get_object { + return shift; +} + +## this is an internal routine. I suggest you don't rely on it's presence. +## sets the realm the user came from. +sub _set_auth_realm { + my ($self, $realmname) = @_; + $self->{'realm'} = $realmname; +} + +## Backwards Compatibility +## you probably want auth_realm, in fact. but this does work for backwards compatibility. +sub store { + my ($self) = @_; + return $self->auth_realm->{store}; +} + __PACKAGE__; __END__ diff --git a/lib/Catalyst/Plugin/Authentication/User/Hash.pm b/lib/Catalyst/Plugin/Authentication/User/Hash.pm index edd6e2c..839f5f4 100644 --- a/lib/Catalyst/Plugin/Authentication/User/Hash.pm +++ b/lib/Catalyst/Plugin/Authentication/User/Hash.pm @@ -24,10 +24,11 @@ sub id { $self->_accessor( "id", @_ ); } -sub store { - my $self = shift; - $self->_accessor( "store", @_ ) || ref $self; -} +## deprecated. Let the base class handle this. +# sub store { +# my $self = shift; +# $self->_accessor( "store", @_ ) || ref $self; +# } sub _accessor { my $self = shift;