Add a parallel authentication routine
Contents
WORK IN PROGRESS
Goal
Let us assume we want to allow our users to log in using their standard eprints username and password, but we want to verify the authenticity of the user via an external agency when they register. We will use an external agency that defines a Persistent User ID (a "PUID"), which is unique and unchanging for each user.
We will then allow the user to actually log in using this PUID, and as a final treat, we will allow the user to change the PUID accociated with their account from one provider to another.
This therefor breaks down into three main tasks:
- Authentication during registration
- Using Authentication as an alternative to normal logging in
- Changing the Authentication PUID for an EPrints account
Explanation in more detail
We want to associate the EPrints user account with an identifying PUID, which allows us to identify the user by EPrints userID or PUID. We will also want to display the organisation (university/department/research-group) that the user is associated with.
We will use a cookie to define which authentication point we want (we might as well make in generic enough to cope with multiple systems)
For security, we will never pass the PUID via URI parameter (POST or GET) as that provides a means of spoofing a user-id
This document will assume that you are creating a new archive rather than modifying a live site. If you want to modify an existing system, you'll need to update the database tables by hand as well as modifying the user object as shown here.
Much of the new code we will be creating will be in a file in archives/ARCHIVEID/cfg/cfg.d/
. Call it something sensible and meaningful. In this example, I'll call it MyCode.pl
Foundation
Prepare the user table
Everything (registration, authentication, alteration, etc) relies on the user tables having the data, without that, nothing will work.
The basic requirement are two data fields in the user object (archives/ARCHIVEID/cfg/cfg.d/user_fields.pl
)
{ 'name' => 'puid', 'type' => 'text', }, { 'name' => 'auth_service', 'type' => 'set', 'options' => [ 'athens', 'shibboleth', 'ldap' ], 'input_style' => 'radio', 'render_quiet' => '1', },
NOTE If you add (or, to a lesser extent, remove) fields to the user_fields.pl file, you need to rebuild the database, which means wiping and restarting (or doing clever stuff with SQL), so make sure you do all this before you start putting data into the repository
The first is an extra field for storing the puid
, the second happens to store the authentication method used but is primarily there for the registration page to be able to pick up.
Editing the registration system to authenticate
Task 1: registration
We need to edit cgi/register
:
- We need to add "auth_service" to the
@sysfields
array.
my @sysfields; unless( $min ) { @sysfields = ( "username", "email", "newpassword", "auth_service" ); } else { @sysfields = ( "email", "newpassword" ); }
NOTE Simply by adding auth_service
to the end of the @sysfields
list, it will be displayed in the registration form, using the phrases defined in <archives/ARCHIVEID/cfg/lang/en/phrases/user_fields.xml/code> (see below)
- Between the do we have the required fields section and the does the email exist section, we add our authentication check:
# Which authentication service to use?
$_ = $v->{auth_service};
my $authorise;
my $uid;
my $orgid;
AUTHO_TYPE:
{
/athens/ && do
{
$authorise = EPrints::YourOrg::Autho::athens_autho($session);
unless ($authorise)
{
return mk_err_page(
$session,
"cgi/register:failed_athens",
$fieldlist,
$v,
);
}
$puid = $authorise->{'userID'};
$orgid = $authorise->{'orgID'};
last AUTHO_TYPE;
};
/shibboleth/ && do
{
$authorise = EPrints::YourOrg::Autho::shibb_autho($session);
unless ( $authorise )
{
return mk_err_page(
$session,
"cgi/register:no_shibb",
$fieldlist,
$v,
);
}
$puid = $authorise->{'userID'};
$orgid = $authorise->{'orgID'};
last AUTHO_TYPE;
};
/ldap/ && do
{
$authorise = EPrints::YourOrg::Autho::ldap_autho($session);
unless ( $authorise )
{
return mk_err_page(
$session,
"cgi/register:no_ldap",
$fieldlist,
$v,
);
}
$puid = $authorise->{'userID'};
$orgid = $authorise->{'orgID'};
last AUTHO_TYPE;
};
} ## end of AUTHO_TYPE switch
# has the puid already been registered
if( defined EPrints::DataObj::User::user_with_puid( $session, $puid ) )
{
return mk_err_page(
$session,
"cgi/register:puid_exists",
$fieldlist,
$v,
{email=>$session->make_text( $puid )}
);
}
The core information in this code is
- that there is a parameter called
auth_service
, which is used to indicate with external authentication system was used,
- that each authentication method returns a reference to a hash,
- the returned hash contains the PUID (and could contain other information),
- each authentication method has an associated error message
- that we need to check that the PUID has not been used with another account.
Having got an acceptable set of details for the new user, we need to add the new details to the user object.. so just before we create the user object, there is a bit more code (insert just before my $user_dataset = $session->get_repository->get_dataset( "user" );
:
$user_data->{puid} = $puid;
# has the puid already been registered
if( ! $inst )
{
return mk_err_page(
$session,
"cgi/register:no_inst",
$fieldlist,
$v,
{email=>$session->make_text( $orgid )} );
}
my $inst = derive_inst_from_orgid($orgid);
$user_data->{org} = $inst;
# OPTIONAL: Log the new user, and how they authenticated
$session->get_repository->log( "Registering user (".$user_data->{username}. ") with puid [". $user_data->{puid}."]" );
# Insert code above here
my $user_dataset = $session->get_repository->get_dataset( "user" );
and lastly, in sub make_reg_form
, we need to define the default option from the auth_service
set:
$defaults->{auth_service} = 'shibboleth';
Authentication routines
The various authentication routines (EPrints::YourOrg::Autho::*
) could live anywhere, however I suggest that you keep them in an area specific to your organisation. In Perl-parlance, EPrints::YourOrg::Autho::foo
means the subroutine foo
in the perl package Autho.pm
, in the directory EPrints/YourOrg/
(or perl_lib/EPrints/YourOrg/Autho.pm
for an eprints install)
How the individual routines work is very dependant on your local environment, however this is how I have done my Shibboleth authentication, using the UK Access management Federation
Phrases
There are a number of new phrases used in this routine. They will all need to be added to <archives/ARCHIVEID/cfg/lang/en/phrases/system.xml/code> - e.g.
<epp:phrase id="cgi/register:failed_athens"><p>Athens Authentication failed.</p></epp:phrase>
<epp:phrase id="cgi/register:no_shibb"><p>UK Access Management Federation Authentication failed.</p></epp:phrase>
<epp:phrase id="cgi/register:puid_exists"><p>When you registered you were bounced through an external <em>authenticating</em> portal. A user exists that has the same <em>Persistent User ID</em> as the one you have given.</p><p>Please <a href="reset_password">reset your password</a>.</p></epp:phrase>
<epp:phrase id="cgi/register:no_inst"><p>Unable to determine the organisation used to authenticate you. Please contact xXx for help.</p></epp:phrase>
and <archives/ARCHIVEID/cfg/lang/en/phrases/user_fields.xml/code> - e.g.
<epp:phrase id="register_fieldopt_auth_service_athens">Athens Authentication Service </epp:phrase>
<epp:phrase id="register_fieldopt_auth_service_shibboleth">The UK Access Management Federation</epp:phrase>
<epp:phrase id="register_fieldopt_auth_service_ldap">Poppleton University's EaSY Sign-on System</epp:phrase>
Finding a user via PUID
We need to confirm that the PUID is not in use. Essentially we replicate the user_with_email and user_with_username routines.
We create this new routine in archives/ARCHIVEID/cfg/cfg.d/myCode.pl
so than we don't lose it if we upgrade the EPrints code-base.
(note how we define the routine as being in a specific package...)
######################################################################
=pod
=item $user = EPrints::DataObj::User::user_with_puid( $session, $puid )
Return the EPrints::user with the specified $puid, or undef if they
are not found.
=cut
######################################################################
sub EPrints::DataObj::User::user_with_puid
{
my( $session, $puid ) = @_;
my $user_ds = $session->get_repository->get_dataset( "user" );
my $searchexp = new EPrints::Search(
session=>$session,
dataset=>$user_ds );
$searchexp->add_field(
$user_ds->get_field( "puid" ),
$puid );
my $searchid = $searchexp->perform_search;
my @records = $searchexp->get_records(0,1);
$searchexp->dispose();
return $records[0];
}
The Login system
Now we have all of our users with an associated PUID, we can turn our attention to the login system
User logins are controlled by a handler (in Mod-Perl speak).
The first thing we need to do is modify the core eprints code to enable an alternative authentication routine: edit perl_lib/EPrints/Apache/Login.pm
:
sub handler
{
my( $r ) = @_;
my $session = new EPrints::Session;
# NEW CODE FRAGMENT
# Do we have a bespoke authentication routine?
if( $session->get_repository->can_call( 'authenticate_user' ) )
{
return $session->get_repository->call( 'authenticate_user', $session, $r );
}
my $problems;
# ok then we need to get the cgi
my $username = $session->param( "login_username" );
my $password = $session->param( "login_password" );
(This may well get rolled into a future release of the core EPrints code)
We now can define our own authentication routine (in archives/ARCHIVEID/cfg/cfg.d/myCode.pl
)
$c->{authenticate_user} = sub {
my ($session, $response) = @_;
##########################
# 3 LINES OF PSEUDO CODE #
##########################
&& get parameters &&
$cookie = eprints_session cookie
unless $cookie { set cookie & reload }
# New code here
my $puid = $session->puid_from_cookie;
if (defined $puid )
{
my $user = EPrints::DataObj::User::user_with_puid( $session,$puid );
if( defined $user )
{
$session->login( $user );
# Log it!
$session->get_repository->log( scalar (gmtime(time()))." Login user (".
$user->get_value('username'). ") with puid [".
$user->get_value('puid')."]" );
return DECLINED;
}
$problems = $session->html_phrase( "cgi/login:puid_failed" );
}
elsif ( defined $username )
{
## cut'n'past from original handler, but with a logger added ##
}
## copy of the "make page" stuff from original handler, without the cookie bit ##
return DONE;
}
There are two new functions in here: $session->puid_from_cookie;
and EPrints::DataObj::User::user_with_puid
. Both of them are also defined in archives/ARCHIVEID/cfg/cfg.d/myCode.pl
++++ working here ++++
and finally
Making the field visable in Modify Profile
To make the field editable, we add it to the user workflow (archives/ARCHIVEID/cfg/workflows/user/default.xml
)
<component type="Field::Multi">
<title><epc:phrase ref="user_section_account" /></title>
<epc:if test="usertype != 'minuser'"><field ref="email"/></epc:if>
<field ref="hideemail"/>
<field ref="password"/>
<field ref="puid" /> <!-- NEW ITEM -->
</component>
and add the field name and help text to our phrases file (archives/ARCHIVEID/cfg/lang/en/phrases/user_fields.xml):
<epp:phrase id="user_fieldname_puid">Devolved Authentication</epp:phrase>
<epp:phrase id="user_fieldhelp_puid">This option determines which (if any)
institution is recognised as providing the correct devolved
authentication for this depositor.</epp:phrase>
changing the way the puid is displayed, and selected
As defined earlier, the puid is a text field in the user object, and will contain a puid in the form of 1234:5678#My+department.
Standard eprints functionality for a text field is to display the contents in an input field, and allow the user to edit it. This is no use to us, as we want the user to select between one of three options:
- None
- Current: Dept 1
- New: Dept 2
(depending, of course, on what is currently set and whether there is a mis-match between user->puid
and the puid from myCookie)
We can alter the way the object deals with a field using local functions. In archives/ARCHIVEID/cfg/cfg.d/user_fields.pl
, we add references to the functions for rendering the field and for changing the value. These two methods return DOM fragments of XML:
{
'name' => 'puid',
'type' => 'text',
'render_single_value' => \&render_puid,
'render_input' => \&select_puid,
},
In archives/ARCHIVEID/cfg/cfg.d/user_fields.pl
we also add the two methods:
sub render_puid
{
my ( $session, $field, $value ) = @_;
if(!EPrints::Utils::is_set($value))
{
return $session->make_text("You have no automatic authentication");
}
# split the value into two parts, on the first # (there may be more than one..)
my ( $rawpuid, $inst) = split /#/, $value, 2;
$inst =~ s/\+/ /g;
if( !EPrints::Utils::is_set($inst) )
{
return $session->make_text(
"You have an institution defined, but it is un-named");
}
$inst =~ s/\+/ /g;
# we should have an institutionl name set, but just in case...
return $session->make_text(
"Your defined authenticating institution is $inst")
}
sub select_puid {
my ( $field, $session, $puid, $dataset, $staff, $hidden_fields, $user,
$basename ) = @_;
my @order = ('none');
my $desc = { none => 'No PUID stuff' };
my $values = { 'none' => '' };
my $default = 'none';
if( EPrints::Utils::is_set( $value ) )
{
push @order, 'old';
$values->{old} = $value;
my ($uid, $org) = split /\#/, $value;
$org =~ s/\+/ /g;
$desc->{old} = "Current value: $org";
$default = 'old';
}
my $cookie_puid = $session->puid_from_cookie;
if( defined $cookie_puid && $cookie_puid ne $value )
{
push @order, 'new';
$values->{new} = $cookie_puid;
my ($uid, $org) = split /\#/, $cookie_puid;
$org =~ s/\+/ /g;
$desc->{new} = "New value: $org";
}
my $div = $session->make_element( "div" );
foreach my $bit ( @order )
{
my $div2 = $session->make_element( "div" );
my $label = $session->make_element( "label" );
$div->appendChild( $div2 );
$div2->appendChild( $label );
my %iopts = (
type=>'radio',
name=>$basename,
value=>$values->{$bit} );
if( $default eq $bit ) { $iopts{checked} = 'checked'; }
$label->appendChild( $session->make_element( "input", %iopts ) );
$label->appendChild( $session->make_text( " ".$desc->{$bit} ) );
}
return $div;
}
Full code
authenticate_user
use: $user = $session->get_repository->call( 'authenticate_user', $session )
Returns the user object having authenticated them in some manner.
Used in EPrints::Apache::Login:
if( $session->get_repository->can_call( 'authenticate_user' ) )
{
return $session->get_repository->call( 'authenticate_user', $session );
}
$c->{authenticate_user} = sub {
my ($session, $response) = @_;
use EPrints::Apache::AnApache;
my $problems;
my $username = $session->param( "login_username" );
my $password = $session->param( "login_password" );
my $pre_auth = $session->param( "login_pre_auth" );
my $cookie = EPrints::Apache::AnApache::cookie( $response, "eprints_session" );
my %opts = ();
unless ( $cookie )
{
# If there is no cookie, we need one (and then re-call this page).
my @a = ();
for(1..16) { push @a, sprintf( "%02X",int rand 256 ); }
$opts{code} = join( "", @a );
$session->set_cookies( %opts );
$session->redirect( $session->get_full_url );
$session->terminate;
return DONE;
}
my $puid = $session->puid_from_cookie;
if (defined $puid )
{
my $user = EPrints::DataObj::User::user_with_puid( $session,$puid );
if( defined $user )
{
$session->login( $user );
$session->get_repository->log( scalar (gmtime(time())).
" Login user (".$user->get_value('username').
") with puid [".
$user->get_value('puid').
"]"
);
return DECLINED;
}
$problems = $session->html_phrase( "cgi/login:puid_failed" );
}
elsif ( defined $username )
{
if( $session->valid_login( $username, $password ) )
{
my $user = EPrints::DataObj::User::user_with_username( $session, $username );
$session->login( $user );
my @time = localtime;
$session->get_repository->log( scalar (gmtime(time())).
" Login user (".
$user->get_value('username').
") locally"
);
my $loginparams = $session->param("loginparams");
my $c = $response->connection;
$c->notes->set( loginparams=>$loginparams );
# Declined to render the HTML, not declined the
# request.
return DECLINED;
}
$problems = $session->html_phrase( "cgi/login:failed" );
} ## end of elsif (username) {}
my $page=$session->make_doc_fragment();
$page->appendChild( EPrints::Apache::Login::input_form( $session, $problems ) );
my $title = $session->make_text( "Login" );
$session->build_page( $title, $page, "login" );
$session->send_page( %opts );
$session->terminate;
return DONE;
}