From: J. Shirley Date: Fri, 7 Nov 2008 20:14:05 +0000 (+0000) Subject: Adding a progressive realm to try multiple realms in a sequence X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=82475b7265ebb00c84d582cec689b4a429286ad5;p=catagits%2FCatalyst-Plugin-Authentication.git Adding a progressive realm to try multiple realms in a sequence --- diff --git a/lib/Catalyst/Authentication/Realm/Progressive.pm b/lib/Catalyst/Authentication/Realm/Progressive.pm new file mode 100644 index 0000000..122dcbc --- /dev/null +++ b/lib/Catalyst/Authentication/Realm/Progressive.pm @@ -0,0 +1,151 @@ +package Catalyst::Authentication::Realm::Progressive; + +use Carp; +use warnings; +use strict; + +use base 'Catalyst::Authentication::Realm'; + +=head1 NAME + +Catalyst::Authentication::Realm::Progressive - Authenticate against multiple realms + +=head1 SYNOPSIS + +This Realm allows an application to be built so that multiple realms are +supported and tried incrementally until a successful authentication. + +A simple use case is a Temporary Password that looks and acts exactly as a +regular password. Without changing the authentication code, you can +authenticate against multiple realms. + +=head2 EXAMPLE + +If your application has multiple realms to authenticate, such as a temporary +password realm and a normal realm, you can configure the progressive realm as +the default, and configure it to iteratively call the temporary realm and then +the normal realm. + + __PACKAGE__->config( + 'Plugin::Authentication' => { + default_realm => 'progressive', + realms => { + progressive => { + class => 'Progressive', + realms => [ 'temp', 'normal' ], + # Modify the authinfo passed into authenticate by merging + # these hashes into the realm's authenticate call: + authinfo_munge => { + 'local' => { 'realm' => 'normal' }, + 'temp' => { 'realm' => 'temp' }, + } + }, + 'normal' => { + credential => { + class => 'Password', + password_field => 'secret', + password_type => 'hashed', + password_hash_type => 'SHA-1', + }, + store => { + class => 'DBIx::Class', + user_class => 'Schema::Person::Identity', + id_field => 'id', + } + }, + temp => { + credential => { + class => 'Password', + password_field => 'secret', + password_type => 'hashed', + password_hash_type => 'SHA-1', + }, + store => { + class => 'DBIx::Class', + user_class => 'Schema::Person::Identity', + id_field => 'id', + } + }, + } + } + ); + +Then, in your controller code, to attempt authentication against both realms +you just have to do a simple authenticate call: + + if ( $c->authenticate({ id => $username, password => $password }) ) { + if ( $c->user->realm eq 'temp' ) { + # Force user to change password + } + } + +=head1 CONFIGURATION + +=over + +=item realms + +An array reference consisting of each realm to attempt authentication against, +in the order listed. If the realm does not exist, calling authenticate will +die. + +=item authinfo_munge + +A hash reference keyed by realm names, with values being hash references to +merge into the authinfo call that is subsequently passed into the realm's +authenticate method. This is useful if your store uses the same class for each +realm, separated by some other token (in the L authinfo_mungesection, +the 'realm' is a column on C that will be either +'temp' or 'local', to ensure the query to fetch the user finds the right +Identity record for that realm. + +=back + +=head1 METHODS + +=head2 authenticate + +This method iteratively calls each realm listed in the C configuration +key. It returns after the first successful authentication call is done. + +=cut + +sub authenticate { + my ( $self, $c, $authinfo ) = @_; + my $realms = $self->config->{realms}; + carp "No realms to authenticate against, check configuration" + unless $realms; + carp "Realms configuration must be an array reference" + unless ref $realms eq 'ARRAY'; + foreach my $realm_name ( @$realms ) { + my $realm = $c->get_auth_realm( $realm_name ); + carp "Unable to find realm: $realm_name, check configuration" + unless $realm; + my $auth = { %$authinfo }; + $auth->{realm} ||= $realm->name; + if ( my $info = $realm->config->{authinfo_munge}->{$realm->name} ) { + $auth = Catalyst::Utils::merge_hashes($auth, $info); + } + if ( my $obj = $realm->authenticate( $c, $auth ) ) { + $c->set_authenticated( $obj, $realm->name ); + return $obj; + } + } + return; +} + +=head1 AUTHORS + +J. Shirley C<< >> + +Jay Kuri C<< >> + +=head1 COPYRIGHT & LICENSE + +Copyright (c) 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. + +=cut + +1; diff --git a/t/lib/AuthRealmTestAppProgressive.pm b/t/lib/AuthRealmTestAppProgressive.pm new file mode 100644 index 0000000..23f12d1 --- /dev/null +++ b/t/lib/AuthRealmTestAppProgressive.pm @@ -0,0 +1,77 @@ +package AuthRealmTestAppProgressive; +use warnings; +use strict; + +### using A::Store::minimal with new style realms +### makes the app blow up, since c::p::a::s::minimal +### isa c:a::s::minimal, and it's compat setup() gets +### run, with an unexpected config has (realms on top, +### not users). This tests makes sure the app no longer +### blows up when this happens. +use Catalyst qw/ + Authentication + Authentication::Store::Minimal +/; + +use Test::More; +use Test::Exception; + +our %members = ( + 'members' => { + bob => { password => "s00p3r" } + }, + 'other' => { + sally => { password => "s00p3r" } + }, +); + +__PACKAGE__->config->{'Plugin::Authentication'} = { + default_realm => 'progressive', + progressive => { + class => 'Progressive', + realms => [ 'other', 'members' ], + }, + other => { + credential => { + class => 'Password', + password_field => 'password', + password_type => 'clear' + }, + store => { + class => 'Minimal', + users => $members{other}, + } + }, + members => { + credential => { + class => 'Password', + password_field => 'password', + password_type => 'clear' + }, + store => { + class => 'Minimal', + users => $members{members}, + } + }, +}; + +sub progressive : Local { + my ( $self, $c ) = @_; + + foreach my $realm ( keys %members ) { + while ( my ( $user, $info ) = each %{$members{$realm}} ) { + my $ok = eval { + $c->authenticate( + { username => $user, password => $info->{password} }, + ); + }; + ok( !$@, "authentication passed." ); + ok( $ok, "user authenticated" ); + ok( $c->user_in_realm($realm), "user in proper realm" ); + } + } + $c->res->body( "ok" ); +} + +__PACKAGE__->setup; + diff --git a/t/live_app_realms_progressive.t b/t/live_app_realms_progressive.t new file mode 100644 index 0000000..bf5fe6b --- /dev/null +++ b/t/live_app_realms_progressive.t @@ -0,0 +1,9 @@ +use strict; +use warnings; + +use Test::More tests => 7; + +use lib 't/lib'; +use Catalyst::Test qw/AuthRealmTestAppProgressive/; + +ok(get("/progressive"), "get ok");