Difference between revisions of "Add a parallel authentication routine"

From EPrints Documentation
Jump to: navigation, search
(Overriding the Login system)
 
(20 intermediate revisions by 3 users not shown)
Line 1: Line 1:
This is a real-world example.
+
[[Category:Manual]]
 +
[[Category:Authentication]]
 +
==WORK IN PROGRESS==
  
==goal==
 
Use an external agency that may define a persistent User ID (a "puid") for someone visiting the repository.
 
  
==explanation==
+
==Goal==
An eprints user may have an identifying puid associated with their account.
+
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.
If a user arrives with a piud that matches one held in the tables, that user profile is loaded. If not, use the standard eprints login system is used to identify the user.
+
We will use an external agency that defines a Persistent User ID (a "PUID"), which is unique and unchanging for each user.
  
Note: puids are opaque, so humans do not recognise the opaque string as meaning anything, and asking a user to type in a puid is prone to typo-errors.
+
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.
''However'', puids are assigned by organisations, so I made sure I got the name of the organisation that authenticated the user along with the puid.
+
  
A puid will be in the format of ''1234:5678#My+Department''
+
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
  
==How To Do It==
+
===Explanation in more detail===
===Prepare the user object===
+
We want to associate the EPrints user account with an identifying PUID, which allows us to identify the user by EPrints userID or PUID.
The basic requirement is a data field in the user object (<code>archives/''ARCHIVEID''/cfg/cfg.d/user_fields.pl</code>)
+
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 <code>archives/''ARCHIVEID''/cfg/cfg.d/</code>. Call it something sensible and meaningful. In this example, I'll call it <code>MyCode.pl</code>
 +
 
 +
===The flow===
 +
 
 +
EPrints authentication Handler is called:
 +
 
 +
* Do we have a current eprints session cookie? (looks in ticket table)
 +
** N - Bounce the user off to get authenticated.
 +
*** user authenticates.
 +
**** Set an eprints session cookie
 +
**** Store an identifier on the "new" auth table
 +
*** bounce user back to their original page
 +
** Y - Can we get user from EPrints ticket table?
 +
*** Y - Get user details, and log them in
 +
*** N - Can we get details from the auth table?
 +
**** N - Bounce user over to manual login pages
 +
**** Y - Can we get a user based on identifier in auth table
 +
***** N - Bounce user over to manual login pages
 +
***** Y - Create record in ticket table
 +
****** bounce user to their original page
 +
 
 +
==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 (<code>archives/''ARCHIVEID''/cfg/cfg.d/user_fields.pl</code>)
 
<pre>
 
<pre>
 
   {
 
   {
Line 21: Line 57:
 
         'type' => 'text',
 
         'type' => 'text',
 
     },
 
     },
 +
    {
 +
        'name'        => 'auth_service',
 +
        'type'        => 'set',
 +
        'options'    => [ 'athens', 'shibboleth', 'ldap' ],
 +
        'input_style' => 'radio',
 +
        'render_quiet' => '1',
 +
    },
 
</pre>
 
</pre>
 
 
'''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
 
'''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
  
===Overriding the Login system===
+
The first is an extra field for storing the <code>puid</code>, the second happens to store the authentication method used but is primarily there for the registration page to be able to pick up.
In EPrints 3.0.3+ you can create a subroutine for an alternative login procedure. Place the following code in <code>archives/''ARCHIVEID''/cfg/cfg.d/myMethods.pl</code>:
+
 +
==Editing the registration system to authenticate==
 +
===Task 1: registration===
 +
We need to edit <code>cgi/register</code>:
 +
* We need to add "auth_service" to the <code>@sysfields</code> array.
 
<pre>
 
<pre>
######################################################################
+
my @sysfields;
=pod
+
unless( $min )
 +
{
 +
@sysfields = ( "username", "email", "newpassword", "auth_service" );
 +
}
 +
else
 +
{
 +
@sysfields = ( "email", "newpassword" );
 +
}
  
if( $self->get_repository->can_call( 'get_current_user' ) )
+
</pre>
{
+
'''NOTE''' Simply by adding <code>auth_service</code> to the end of the <code>@sysfields</code> list, it will be displayed in the registration form, using the phrases defined in <code><archives/''ARCHIVEID''/cfg/lang/en/phrases/user_fields.xml/code> (see below)
  $self->{current_user} = $self->get_repository->call( 'get_current_user', $self );
+
}
+
  
An optional call that allows one to get the current eprints user-object
+
* Between the ''do we have the required fields'' section and the ''does the email exist'' section, we add our authentication check:
by some method.
+
<pre>
 +
  # Which authentication service to use?
 +
  $_ = $v->{auth_service};
 +
  my $authorise;
 +
  my $uid;
 +
  my $inst;
  
If this method is available, it is called B<instead of> the internal
+
AUTHO_TYPE:
eprint methods.
+
 +
  /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'};
 +
    $inst  = $authorise->{'institution'}; 
 +
    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'};
 +
    $inst  = $authorise->{'institution'}; 
 +
    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'};
 +
    $inst  = $authorise->{'institution'}; 
 +
    last AUTHO_TYPE;
 +
  };
 +
} ## end of AUTHO_TYPE switch
  
=cut
 
######################################################################
 
$c->{get_current_user} = sub {
 
  my( $session ) = @_;
 
  
   if( !defined $session->{request} )
+
  # has the puid already been registered
 +
   if( defined EPrints::DataObj::User::user_with_puid( $session, $puid ) )
 
   {
 
   {
     # not a cgi script.
+
     return mk_err_page(
     return undef;
+
      $session,
 +
      "cgi/register:puid_exists",
 +
      $fieldlist,
 +
      $v,
 +
      {email=>$session->make_text( $puid )}
 +
     );
 
   }
 
   }
  # check for a eprints session cookie
+
</pre>
  my $user = $session->_current_user_auth_cookie;
+
  if( defined $user )
+
  {
+
    return $user;
+
  }
+
  # get my puid
+
  my $puid = $session->puid_from_cookie;
+
  
  return undef unless defined $puid;
+
The core information in this code is
 +
* that there is a parameter called <code>auth_service</code>, 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.
  
  # can we get a user?
+
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 <code>my $user_dataset = $session->get_repository->get_dataset( "user" );</code>:
  my $user = EPrints::DataObj::User::user_with_puid( $session,$puid );
+
<pre>
  if( defined $user )
+
  {
+
    $session->{via_depot} = 1;
+
  }
+
  return $user;
+
};
+
  
</pre>
+
  $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( $inst )} );
 +
}
 +
  $user_data->{org} = $inst;
  
There are two new functions in here: <code>$session->puid_from_cookie;</code> and  <code>EPrints::DataObj::User::user_with_puid</code>. Both of them are also defined in <code>archives/''ARCHIVEID''/cfg/cfg.d/myMethods.pl</code>
+
  # OPTIONAL: Log the new user, and how they authenticated
<pre>
+
  $session->get_repository->log( "Registering user (".$user_data->{username}. ") with puid [". $user_data->{puid}."]" );
######################################################################
+
=pod
+
  
=item $puid = $session->puid_from_cookie;
+
  # Insert code above here
 +
  my $user_dataset = $session->get_repository->get_dataset( "user" );
  
returns the puid from our own cookie
+
</pre>
  
Used in both the dynamic template and C<&get_current_user():>
+
and lastly, in <code>sub make_reg_form</code>, we need to define the default option from the <code>auth_service</code> set:
 +
<pre>
 +
$defaults->{auth_service} = 'shibboleth';
 +
</pre>
  
=cut
+
===Authentication routines===
######################################################################
+
The various authentication routines (<code>EPrints::YourOrg::Autho::*</code>) could live anywhere, however I suggest that you keep them in an area specific to your organisation. In Perl-parlance, <code>EPrints::YourOrg::Autho::foo</code> means the subroutine ''<code>foo</code>'' in the perl package ''<code>Autho.pm</code>'', in the directory ''<code>EPrints/YourOrg/</code>'' (<code>or perl_lib/EPrints/YourOrg/Autho.pm</code> for an eprints install)
sub EPrints::Session::puid_from_cookie
+
{
+
  my( $self ) = @_;
+
  
  my $cookie = EPrints::Apache::AnApache::cookie( $self->get_request, "myCookie" );
+
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
  if( !defined $cookie ) { return undef; }
+
  my %info = split( '&', $cookie );
+
  # do cookie validation checks here
+
  my $puid = $info{user};
+
  $puid=~s/\%([0-9A-F]{2})/eval "chr(0x$1)"/egi;
+
  
  return $puid;
+
===Phrases===
}
+
There are a number of new ''phrases'' used in this routine. They will all need to be added to <code><archives/''ARCHIVEID''/cfg/lang/en/phrases/system.xml/code> - e.g.
 +
<pre>
 +
    <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>
 +
</pre>
 +
and <code><archives/''ARCHIVEID''/cfg/lang/en/phrases/user_fields.xml/code> - e.g.
 +
<pre>
 +
<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>
 +
</pre>
 +
 
 +
===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 <code>archives/''ARCHIVEID''/cfg/cfg.d/myCode.pl</code> 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...)
 +
<pre>
 
######################################################################
 
######################################################################
 
=pod
 
=pod
Line 132: Line 256:
 
</pre>
 
</pre>
  
and finally <code>$session->{via_depot} = 1</code> just sets a flag in the session.
+
==The Login system==
 +
Now we have all of our users with an associated PUID, we can turn our attention to the login system
  
===Tidy up on logout===
+
User logins are controlled by a ''handler'' (in Mod-Perl speak).
We need to account for users who are working from public computers: we can't leave our authentication cookie lying around for someone else to ''accidentally'' pick up on, so lets provide extra log-out functionality (again, eprints 3.0.3+).
+
Another function for <code>archives/''ARCHIVEID''/cfg/cfg.d/myMethods.pl</code>:
+
<pre>
+
######################################################################
+
=pod
+
 
+
  if( $session->get_repository->can_call( 'on_logout' ) )
+
  {
+
    $session->get_repository->call( 'on_logout', $session );
+
  }
+
  
A method that can be called as part of the logout routine.
+
The first thing we need to do is modify the core eprints code to enable an alternative authentication routine: edit <code>perl_lib/EPrints/Apache/Login.pm</code>:
  
It happens before any of the standard EPrints logout routines are called.
+
<pre>
 
+
sub handler
=cut
+
######################################################################
+
 
+
$c->{on_logout} = sub
+
 
{
 
{
  my( $session ) = @_;
+
my( $r ) = @_;
## clear the Repository Junction cookie
+
  my $cookie = $session->{query}->cookie(
+
                  -name => 'depot_authen',
+
                  -domain => 'edina.ac.uk',
+
                  -expires => time(),
+
                  -value => '',
+
                  );
+
  EPrints::Apache::AnApache::header_out(
+
$session->{"request"},
+
"Set-Cookie" => $cookie );
+
};
+
  
 +
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" );
 
</pre>
 
</pre>
 +
(This may well get rolled into a future release of the core EPrints code)
  
===Displaying the method of logging in===
+
We now can define our own authentication routine (in <code>archives/''ARCHIVEID''/cfg/cfg.d/myCode.pl</code>)
It may be beneficial for our users to know how they have logged in. This is done in the dynamic template: <code>archives/''ARCHIVEID''/cfg/cfg.d/dynamic_template.pl</code>
+
 
<pre>
 
<pre>
$c->{dynamic_template}->{function} = sub
 
{
 
  my( $session, $parts ) = @_;
 
  
   my $user = $session->current_user;
+
$c->{authenticate_user} = sub {
   if( defined $user )
+
   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 )
 
   {
 
   {
    # new stuff, only used if there is a current user
+
     my $user = EPrints::DataObj::User::user_with_puid( $session,$puid );
     my $cookie_puid = $session->puid_from_cookie;
+
    if( defined $user )
    my $phrase_id = 'dynamic:logged_in_local';
+
    if( defined $cookie_puid )
+
    {
+
      my $user_puid = $user->get_value( "puid" );
+
      if( $cookie_puid eq $user_puid )
+
      {
+
        $phrase_id = 'dynamic:logged_in_puid';
+
      }
+
      else
+
      {
+
        $phrase_id = 'dynamic:logged_in_local_mismatch';
+
      }
+
    }
+
 
+
    # Normal routine
+
    $parts->{login_status} = $session->html_phrase(
+
"dynamic:logged_in",
+
user => $session->html_phrase( $phrase_id, user=>$user->render_description ),
+
tools => render_toolbar($session) );
+
    }
+
    else
+
 
     {
 
     {
       $parts->{login_status} = $session->html_phrase(  
+
       $session->login( $user );
"dynamic:not_logged_in" );
+
      # 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;
 +
}
 
</pre>
 
</pre>
  
==Allowing the user to change the puid==
+
There are two new functions in here: <code>$session->puid_from_cookie;</code> and  <code>EPrints::DataObj::User::user_with_puid</code>. Both of them are also defined in <code>archives/''ARCHIVEID''/cfg/cfg.d/myCode.pl</code>
We have described a situation where user account is associated with a single PUID.
+
 
 +
 
 +
++++ working here ++++
 +
 
 +
and finally
  
This is fine, and if the "puid" is defined by the administrators and never changes, then the field can be left out of the user workflow, and the user will never need know of its existance.
 
  
There are situations where a user may come in with a different puid (visiting a different department, moving to a different department, a change at institutional level, etc), and a user should be able to change the puid associated with their account.
 
  
We can identify three possible senarios for a user logging into the repository:
 
1) The assumed-to-be-default method is via whatever your devolved authentication system, where ''myCookie'' identifies the user, and logs them in automatically
 
2) The user may arrive at your repository directly, and wish to log in. There is no ''myCookie'', so the user enters their login name and password.
 
3) The user comes in a valid ''myCookie'', but has a PUID which is not recognised by the repository, so they need to login as per method 2 (but noting that ''myCookie'' is set)
 
  
Once logged in, the user may modify their profile.
 
  
 
=== Making the field visable in ''Modify Profile''===
 
=== Making the field visable in ''Modify Profile''===
Line 274: Line 385:
 
# split the value into two parts, on the first # (there may be more than one..)
 
# split the value into two parts, on the first # (there may be more than one..)
 
my ( $rawpuid, $inst) = split /#/, $value, 2;  
 
my ( $rawpuid, $inst) = split /#/, $value, 2;  
+
$inst =~ s/\+/ /g;
 +
 
 
if( !EPrints::Utils::is_set($inst) )
 
if( !EPrints::Utils::is_set($inst) )
 
{
 
{
Line 339: Line 451:
 
}
 
}
 
</pre>
 
</pre>
 +
 +
== 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 );
 +
  }
 +
 +
<pre>
 +
$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;
 +
}
 +
</pre>
 +
 +
[[Category:Authentication]]

Latest revision as of 12:45, 20 March 2010

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:

  1. Authentication during registration
  2. Using Authentication as an alternative to normal logging in
  3. 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

The flow

EPrints authentication Handler is called:

  • Do we have a current eprints session cookie? (looks in ticket table)
    • N - Bounce the user off to get authenticated.
      • user authenticates.
        • Set an eprints session cookie
        • Store an identifier on the "new" auth table
      • bounce user back to their original page
    • Y - Can we get user from EPrints ticket table?
      • Y - Get user details, and log them in
      • N - Can we get details from the auth table?
        • N - Bounce user over to manual login pages
        • Y - Can we get a user based on identifier in auth table
          • N - Bounce user over to manual login pages
          • Y - Create record in ticket table
            • bounce user to their original page

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 $inst;

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'};
    $inst  = $authorise->{'institution'};  
    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'};
    $inst  = $authorise->{'institution'};  
    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'};
    $inst  = $authorise->{'institution'};  
    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 <code>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( $inst )} );
	}
  $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 <code><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 <code>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;
}