Difference between revisions of "Add a parallel authentication routine"

From EPrints Documentation
Jump to: navigation, search
Line 1: Line 1:
This is a real-world example.
+
==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''
+
===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===
+
 
The basic requirement is a data field in the user object (<code>archives/''ARCHIVEID''/cfg/cfg.d/user_fields.pl</code>)
+
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>
 +
 
 +
==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 31:
 
         '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===
+
==Editing the registration system to authenticate==
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>:
+
===/cgi/register===
 +
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>
{
 
  $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 $orgid;
  
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,
 +
      );
 +
    }
 +
    $v->{puid} = $authorise->{'PersistenUserID'};
 +
    $orgid = $authorise->{'OrganisationID'}; 
 +
    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,
 +
        );
 +
    }
 +
    $v->{puid} = $authorise->{'PersistentUserID'};
 +
    $orgid = $authorise->{'OrganisationID'}; 
 +
    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,
 +
        );
 +
    }
 +
    $v->{puid} = $authorise->{'PersistentUserID'};
 +
    $orgid = $authorise->{'OrganisationID'}; 
 +
    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, $v->{puid} ) )
 
   {
 
   {
     # not a cgi script.
+
     return mk_err_page(
     return undef;
+
      $session,
 +
      "cgi/register:puid_exists",
 +
      $fieldlist,
 +
      $v,
 +
      {email=>$session->make_text( $v->{puid} )}
 +
     );
 
   }
 
   }
  # check for a eprints session cookie
+
</pre>
   my $user = $session->_current_user_auth_cookie;
+
 
   if( defined $user )
+
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,
    return $user;
+
* that each authentication method returns a reference to a hash,
  }
+
* the returned hash contains the PUID (and could contain other information),
   # get my puid
+
* each authentication method has an associated error message
   my $puid = $session->puid_from_cookie;
+
* 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>
 +
   $user_data->{puid} = $v->{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;
  
   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>
  
=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 132: Line 218:
 
</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
 +
 
 +
++++ continue from here ++++
 +
 
 +
The first thing we need to do is extend the login procedure: edit <code>perl_lib/EPrints/Apache/Login.pm</code>:
 +
 
 +
<pre>
 +
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" );
 +
</pre>
 +
 
 +
We now need to define our own authentication routine (in <code>archives/''ARCHIVEID''/cfg/cfg.d/MyCode.pl</code>)
 +
<pre>
 +
######################################################################
 +
MOSTLY PSEUDO CODE
 +
######################################################################
 +
 
 +
$c->{authenticate_user} = sub {
 +
  my ($session, $response) = @_;
 +
 
 +
  && 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;
 +
}
 +
</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 finally
  
 
===Tidy up on logout===
 
===Tidy up on logout===
Line 170: Line 328:
 
</pre>
 
</pre>
  
===Displaying the method of logging in===
 
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>
 
$c->{dynamic_template}->{function} = sub
 
{
 
  my( $session, $parts ) = @_;
 
 
  my $user = $session->current_user;
 
  if( defined $user )
 
  {
 
    # new stuff, only used if there is a current user
 
    my $cookie_puid = $session->puid_from_cookie;
 
    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(
 
"dynamic:not_logged_in" );
 
    }
 
};
 
 
</pre>
 
(and remember to add <code>dynamic:logged_in_puid</code> ''et al'' to the appropriate phrases file)
 
 
==Allowing the user to change the puid==
 
We have described a situation where user account is associated with a single PUID.
 
 
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:
 
# The assumed-to-be-default method is via whatever your devolved authentication system, where ''myCookie'' identifies the user, and logs them in automatically
 
# 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.
 
# 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 342: Line 447:
 
</pre>
 
</pre>
  
== A final twist: take "organisation" from puid ==
+
== Full code ==
We know that a puid is in the form ''1234:5678#My+department'', and we have already seen (in <code>select_puid</code>) that we can grab the organisation from this puid - so the logical extension is to set the users organisation field from the puid. This is actually quite easy to do!
+
=== authenticate_user ===
 +
######################################################################
 +
=pod
 +
 
 +
=item $user = $session->get_repository->call( 'authenticate_user', $session )
 +
 
 +
Returns the user object having authenticated them in some manner.
  
In <code>archives/''ARCHIVEID''/cfg/cfg.d/user_fields.pl</code>, we add references to a function that over-rides the default method for dealing with a value in the users <code>org</code> field:
+
Used in EPrints::Apache::Login:
<pre>
 
    {
 
        'name' => 'org',
 
        'type' => 'text',
 
        'fromform' => \&update_org,
 
    },
 
</pre>
 
... and add the function to <code>archives/''ARCHIVEID''/cfg/cfg.d/user_fields.pl</code> as before:
 
<pre>
 
sub update_org
 
{
 
  my ($value, $session ) = @_;
 
  
   unless ($value)
+
   if( $session->get_repository->can_call( 'authenticate_user' ) )
 
   {
 
   {
    my $puid =  $session->current_user->get_value( 'puid' );
+
      return $session->get_repository->call( 'authenticate_user', $session );
    my ($puid, $value) = split /\#/, $puid;
 
    $value =~ s/\+/ /g;  
 
 
   }
 
   }
   return $value;  
+
 
}</pre>
+
 
 +
=cut
 +
######################################################################
 +
$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;
 +
    }
 +
#    # Authority token passed in
 +
      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 )
 +
  {
 +
      # we've be passed a 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;
 +
 
 +
}

Revision as of 10:14, 25 April 2008

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.

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

Editing the registration system to authenticate

/cgi/register

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" );
	}

  • 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,
      );
    }
    $v->{puid} = $authorise->{'PersistenUserID'};
    $orgid = $authorise->{'OrganisationID'};  
    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,
        );
    }
    $v->{puid} = $authorise->{'PersistentUserID'};
    $orgid = $authorise->{'OrganisationID'};  
    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,
        );
    }
    $v->{puid} = $authorise->{'PersistentUserID'};
    $orgid = $authorise->{'OrganisationID'};  
    last AUTHO_TYPE;
  }; 
} ## end of AUTHO_TYPE switch


  # has the puid already been registered
  if( defined EPrints::DataObj::User::user_with_puid( $session, $v->{puid} ) )
  {
    return mk_err_page( 
      $session,
      "cgi/register:puid_exists", 
      $fieldlist,
      $v,
      {email=>$session->make_text( $v->{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} = $v->{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>

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

++++ continue from here ++++

The first thing we need to do is extend the login procedure: 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" );

We now need to define our own authentication routine (in archives/ARCHIVEID/cfg/cfg.d/MyCode.pl)

######################################################################
 MOSTLY PSEUDO CODE
######################################################################

$c->{authenticate_user} = sub {
  my ($session, $response) = @_;
  
  && 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/myMethods.pl


and finally

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 archives/ARCHIVEID/cfg/cfg.d/myMethods.pl:

######################################################################
=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.

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 => 'myCookie',
                   -domain => 'my.domain.foo',
                   -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 (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

=pod

=item $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 );
 }


=cut

$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;
   }
  1. # Authority token passed in
     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 )

{

     # we've be passed a 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;

}