Difference between revisions of "Add a parallel authentication routine"

From EPrints Documentation
Jump to: navigation, search
m
 
(28 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.
  
There are situations where a user may come in with a different puid (visiting a different institution, moving to a different institution, a change at institutional level, etc), and a user should be able to change the puid associated with their account.
+
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.
  
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.
+
This therefor breaks down into three main tasks:
''However'', puids are assigned by an institution, so I made sure I got the name of the institution that authenticated the user along with the puid.
+
# Authentication during registration
 +
# Using Authentication as an alternative to normal logging in
 +
# Changing the Authentication PUID for an EPrints account
  
A puid will be in the format of ''1234:5678#My+Institution''
+
===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.
  
==How To Do It==
+
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)
===Prepare the user object===
+
 
Step 1 is to set up the basic requirement: We need fields in the user object (archives/''ARCHIVEID''/cfg/cfg.d/user_fields.pl)
+
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 23: Line 57:
 
         'type' => 'text',
 
         'type' => 'text',
 
     },
 
     },
 +
    {
 +
        'name'        => 'auth_service',
 +
        'type'        => 'set',
 +
        'options'    => [ 'athens', 'shibboleth', 'ldap' ],
 +
        'input_style' => 'radio',
 +
        'render_quiet' => '1',
 +
    },
 
</pre>
 
</pre>
and the corresponding entries in the workflow and phrases files:
+
'''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
  
Workflows (archives/''ARCHIVEID''/cfg/workflows/user/default.xml)
+
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.
 +
 +
==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>
    <component type="Field::Multi">
+
my @sysfields;
      <title><epc:phrase ref="user_section_account" /></title>
+
unless( $min )
      <epc:if test="usertype != 'minuser'"><field ref="email"/></epc:if>
+
{
      <field ref="hideemail"/>
+
@sysfields = ( "username", "email", "newpassword", "auth_service" );
      <field ref="password"/>
+
}
      <field ref="puid" /> <!-- NEW ITEM -->
+
else
    </component>
+
{
 +
@sysfields = ( "email", "newpassword" );
 +
}
 +
 
 
</pre>
 
</pre>
phrases (archives/''ARCHIVEID''/cfg/lang/en/phrases/user_fields.xml):
+
'''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)
<pre>
 
    <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>
 
</pre>
 
'''NOTE''' If you alter 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===
+
* Between the ''do we have the required fields'' section and the ''does the email exist'' section, we add our authentication check:
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>:
 
 
<pre>
 
<pre>
######################################################################
+
  # Which authentication service to use?
=pod
+
  $_ = $v->{auth_service};
 +
  my $authorise;
 +
  my $uid;
 +
  my $inst;
  
  if( $self->get_repository->can_call( 'get_current_user' ) )
+
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
 
  {
 
  {
  $self->{current_user} = $self->get_repository->call( 'get_current_user', $self );
+
    $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
  
An optional call that allows one to get the current eprints user-object
 
by some method.
 
  
If this method is available, it is called B<instead of> the internal
+
  # has the puid already been registered
eprint methods.
+
  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 )}
 +
    );
 +
  }
 +
</pre>
  
=cut
+
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,
$c->{get_current_user} = sub {
+
* that each authentication method returns a reference to a hash,
my( $session ) = @_;
+
* 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 <code>my $user_dataset = $session->get_repository->get_dataset( "user" );</code>:
 +
<pre>
  
if( !defined $session->{request} )
+
  $user_data->{puid} = $puid;
 +
 
 +
  # has the puid already been registered
 +
if( ! $inst )
 
{
 
{
# not a cgi script.
+
return mk_err_page(
return undef;
+
$session,
 +
"cgi/register:no_inst",
 +
$fieldlist,
 +
$v,
 +
{email=>$session->make_text( $inst )} );
 
}
 
}
   # check for a eprints session cookie
+
   $user_data->{org} = $inst;
  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;
+
   # OPTIONAL: Log the new user, and how they authenticated
 +
  $session->get_repository->log( "Registering user (".$user_data->{username}. ") with puid [". $user_data->{puid}."]" );
  
   # can we get a user?
+
   # Insert code above here
   my $user = EPrints::DataObj::User::user_with_puid( $session,$puid );
+
   my $user_dataset = $session->get_repository->get_dataset( "user" );
  if( defined $user )
 
  {
 
    $session->{via_depot} = 1;
 
  }
 
  return $user;
 
};
 
  
 
</pre>
 
</pre>
  
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>
+
and lastly, in <code>sub make_reg_form</code>, we need to define the default option from the <code>auth_service</code> set:
 
<pre>
 
<pre>
######################################################################
+
$defaults->{auth_service} = 'shibboleth';
=pod
+
</pre>
  
=item $puid = $session->puid_from_cookie;
+
===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)
  
returns the puid from our own cookie
+
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
  
Used in both the dynamic template and C<&get_current_user():>
+
===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>
  
=cut
+
===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.
sub EPrints::Session::puid_from_cookie
+
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...)
  my( $self ) = @_;
+
<pre>
 
 
  my $cookie = EPrints::Apache::AnApache::cookie( $self->get_request, "myCookie" );
 
  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;
 
}
 
 
######################################################################
 
######################################################################
 
=pod
 
=pod
Line 152: 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
 +
 
 +
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 <code>perl_lib/EPrints/Apache/Login.pm</code>:
  
===Tidy up on logout===
 
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>
 
<pre>
######################################################################
+
sub handler
=pod
+
{
 +
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" );
 +
</pre>
 +
(This may well get rolled into a future release of the core EPrints code)
 +
 
 +
We now can define our own authentication routine (in <code>archives/''ARCHIVEID''/cfg/cfg.d/myCode.pl</code>)
 +
<pre>
 +
 
 +
$c->{authenticate_user} = sub {
 +
  my ($session, $response) = @_;
 +
 
 +
  ##########################
 +
  # 3 LINES OF PSEUDO CODE #
 +
  ##########################
 +
  && get parameters &&
 +
  $cookie = eprints_session cookie
 +
  unless $cookie { set cookie & reload }
  
   if( $session->get_repository->can_call( 'on_logout' ) )
+
  # 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 )
 
   {
 
   {
    $session->get_repository->call( 'on_logout', $session );
+
      ## cut'n'past from original handler, but with a logger added ##
 
   }
 
   }
  
A method that can be called as part of the logout routine.
+
  ## copy of the "make page" stuff from original handler, without the cookie bit ##
 +
 
 +
  return DONE;
 +
}
 +
</pre>
 +
 
 +
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>
 +
 
 +
 
 +
++++ working here ++++
 +
 
 +
and finally
 +
 
  
It happens before any of the standard EPrints logout routines are called.
 
  
=cut
 
######################################################################
 
  
$c->{on_logout} = sub
 
{
 
  my( $session ) = @_;
 
## 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 );
 
};
 
  
 +
=== Making the field visable in ''Modify Profile''===
 +
To make the field ''editable'', we add it to the ''user workflow'' (<code>archives/''ARCHIVEID''/cfg/workflows/user/default.xml</code>)
 +
<pre>
 +
    <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>
 +
</pre>
 +
and add the field name and help text to our phrases file (archives/''ARCHIVEID''/cfg/lang/en/phrases/user_fields.xml):
 +
<pre>
 +
    <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>
 
</pre>
 
</pre>
  
== needs re-edited below ==
+
=== changing the way the puid is displayed, and selected ===
====Field ''rendering'' Problem====
+
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''.
However, this means that we get a text input item, not what we want (see the comment above about users typing in opaque strings)
+
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 <code>user->puid</code> and the puid from ''myCookie'')
  
===Changing the way the field is ''rendered''===
+
We can alter the way the object deals with a field using local functions. In <code>archives/''ARCHIVEID''/cfg/cfg.d/user_fields.pl</code>, we add references to the functions for rendering the field and for changing the value. These two methods return DOM fragments of XML:
In Step 2 we alter the way the object deals with the 'puid' field. the user field is changed and we define alternative methods for rendering the field (as a fragment of xhtml) and for changing the value:
 
 
<pre>
 
<pre>
 
     {
 
     {
Line 217: 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 282: 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 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 <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;
}