Difference between revisions of "Add a parallel authentication routine"

From EPrints Documentation
Jump to: navigation, search
m (Changing the way the field is ''rendered'')
 
(37 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
  
==How To Do It==
+
===Explanation in more detail===
===The very basics===
+
We want to associate the EPrints user account with an identifying PUID, which allows us to identify the user by EPrints userID or PUID.
Step 1 is to set up the basic requirement: We need fields in the user object (archives/''ARCHIVEID''/cfg/cfg.d/user_fields.pl)
+
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>
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
 +
 
 +
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>
 +
my @sysfields;
 +
unless( $min )
 +
{
 +
@sysfields = ( "username", "email", "newpassword", "auth_service" );
 +
}
 +
else
 +
{
 +
@sysfields = ( "email", "newpassword" );
 +
}
 +
 
 +
</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)
 +
 
 +
* Between the ''do we have the required fields'' section and the ''does the email exist'' section, we add our authentication check:
 +
<pre>
 +
  # 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 )}
 +
    );
 +
  }
 +
</pre>
 +
 
 +
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.
 +
 
 +
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} = $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" );
 +
 
 +
</pre>
 +
 
 +
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>
 +
 
 +
===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)
 +
 
 +
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 <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
 +
 
 +
=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];
 +
}
 +
</pre>
 +
 
 +
==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>:
 +
 
 +
<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>
 +
(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 }
 +
 
 +
  # 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/myCode.pl</code>
 +
 
 +
 
 +
++++ working here ++++
 +
 
 +
and finally
 +
 
 +
 
 +
 
 +
 
  
Workflows (archives/''ARCHIVEID''/cfg/workflows/user/default.xml)
+
=== 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>
 
<pre>
 
     <component type="Field::Multi">
 
     <component type="Field::Multi">
Line 34: Line 347:
 
     </component>
 
     </component>
 
</pre>
 
</pre>
phrases (archives/''ARCHIVEID''/cfg/lang/en/phrases/user_fields.xml):
+
and add the field name and help text to our phrases file (archives/''ARCHIVEID''/cfg/lang/en/phrases/user_fields.xml):
 
<pre>
 
<pre>
 
     <epp:phrase id="user_fieldname_puid">Devolved Authentication</epp:phrase>
 
     <epp:phrase id="user_fieldname_puid">Devolved Authentication</epp:phrase>
Line 41: Line 354:
 
                   authentication for this depositor.</epp:phrase>
 
                   authentication for this depositor.</epp:phrase>
 
</pre>
 
</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
 
  
====Field ''rendering'' Problem====
+
=== changing the way the puid is displayed, and selected ===
However, this means that we get a text input item, not what we want (see the comment above about users typing in opaque strings)
+
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 <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 69: 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 135: Line 452:
 
</pre>
 
</pre>
  
====Problem: needs data set at the beginning of each session====
+
== Full code ==
This works well, however there is a small snag - how are we setting <code>$c->notes->get('this_puid');</code>?
+
=== authenticate_user ===
  
====Altering Session.pm====
+
use: $user = $session->get_repository->call( 'authenticate_user', $session )
Well, this is all done in <code>perl_lib/EPrints/Session.pm</code>, specifically in <code>_current_user_auth_cookie</code>:
+
<pre>
+
sub _current_user_auth_cookie
+
{
+
my( $self ) = @_;
+
  
if( !defined $self->{request} )
+
Returns the user object having authenticated them in some manner.
{
+
# not a cgi script.
+
return undef;
+
}
+
  
  # Hack to ensure that the Repository Junction details are set before
+
Used in EPrints::Apache::Login:
  # the local EP3 cookie stuff takes over
+
  my %cookie;
+
  my ($puid, $orgname);
+
 
+
# we won't have the cookie for the page after login.
+
my $c = $self->{request}->connection;
+
  
   unless ( $c->notes->get( "this_puid" ) ) {
+
   if( $session->get_repository->can_call( 'authenticate_user' ) )
    warn "Not got RJunction cookie";
+
  {
    my $rj_cookie = EPrints::Apache::AnApache::cookie(  $self->get_request, "depot_authen" );
+
       return $session->get_repository->call( 'authenticate_user', $session );
 
+
    if( defined $rj_cookie ) {
+
       %cookie = split /&/, $rj_cookie;
+
      ## Do clever stuff to extract $puid and $orgname from the cookie ##
+
      $c->notes->set( "this_puid", $puid );
+
      $c->notes->set( $puid, $orgname );
+
    } ## end of cookie defined
+
  } ## end of "if note set"
+
  ## end of first bit of hack, back to normal, but the coding is slightly altered
+
 
+
  my $userid = $c->notes->get( "userid" );
+
  $c->notes->set( "userid", 'undef' );
+
 
+
  my $user;
+
 
+
  # Check for a userid set in the session notes area
+
  if( EPrints::Utils::is_set( $userid ) && $userid ne 'undef' )
+
  {
+
    $user = EPrints::DataObj::User->new( $self, $userid );
+
 
   }
 
   }
 
 
  # if not, check for an EPrints user session cookie
 
  unless ( $user ) {
 
    my $cookie = EPrints::Apache::AnApache::cookie( $self->get_request, "eprints_session" );
 
    if ( $cookie  ) {
 
      my $remote_addr = $c->get_remote_host;
 
      $userid = $self->{database}->get_ticket_userid( $cookie, $remote_addr );
 
      $user = EPrints::DataObj::User->new( $self, $userid ) if EPrints::Utils::is_set( $userid );
 
    }
 
  }
 
  
  # If still not, can we authenticate with a puid?
+
<pre>
  unless ( $user ) {
+
$c->{authenticate_user} = sub {
    if ( $c->notes->get( "this_puid" ) ) {
+
   my ($session, $response) = @_;
      $user = EPrints::DataObj::User::user_with_puid( $self, $c->notes->get( "this_puid" ) );
+
    }
+
   }
+
+
  # if we get a user, by hook or by crook, set some details in the notes section
+
  if ( $user ) {
+
    if ( $user->get_value('puid') ) {
+
      $c->notes->set( 'puid' , $user->get_value('puid' ) );
+
      $c->notes->set( $user->get_value('puid') ,
+
                      $user->get_value('puid_inst') );
+
    }
+
    return $user;
+
  }
+
 
    
 
    
  # if we really cannot find a user, return undef (no user found)
+
    use EPrints::Apache::AnApache;   
  return undef;
+
   
}
+
    my $problems;
</pre>
+
    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 );
  
===So how do I save ''two'' fields at once?===
+
          $session->get_repository->log( scalar (gmtime(time())).
There is one small problem, although we can change the puid held in the user object, we're not updating the nice piece of text held in <code>puid_inst</code>
+
                                        " Login user (".$user->get_value('username').
 
+
                                        ") with puid [".
====Altering the way the submitted data is handled====
+
                                        $user->get_value('puid').
Step 3 is to save the name of the authenticting institute as well as the puid, so that we can display the appropriate name.
+
                                        "]"
 
+
                                      );
We need another subroutine in archives/''ARCHIVEID''/cfg/cfg.d/user_fields.pl:
+
          return DECLINED;
<pre>
+
        }
 +
        $problems = $session->html_phrase( "cgi/login:puid_failed" );
 +
      }
 +
    elsif ( defined $username )
 
     {
 
     {
        'name'                => 'puid',
+
      if( $session->valid_login( $username, $password ) )
         'type'                => 'text',
+
      {
         'render_single_value' => \&render_puid,
+
         my $user = EPrints::DataObj::User::user_with_username( $session, $username );
         'render_input'        => \&select_puid,
+
         $session->login( $user );
      ''' 'fromform'            => \&fromform_puid,'''
+
         my @time = localtime;      
     },
+
        $session->get_repository->log(  scalar (gmtime(time())).
</pre>
+
                                        " Login user (".
 
+
                                        $user->get_value('username').
and the corresponding method:
+
                                        ") locally"
<pre>
+
                                    );
sub fromform_puid {
+
        my $loginparams = $session->param("loginparams");
    my ( $value, $session ) = @_;
+
        my $c = $response->connection;
   
+
        $c->notes->set( loginparams=>$loginparams );
    # get the user object from the session
+
        # Declined to render the HTML, not declined the
    my $c = $session->{request}->connection;
+
        # request.
    my $inst = $c->notes->get($value);
+
        return DECLINED;
    my $user = $session->current_user;
+
      }
 
+
      $problems = $session->html_phrase( "cgi/login:failed" );
    # save the text associated with the puid in the sesion notes
+
  } ## end of elsif (username) {}
    $user->set_value('puid_inst', $inst );
+
  my $page=$session->make_doc_fragment();
    $user->commit;
+
  $page->appendChild( EPrints::Apache::Login::input_form( $session, $problems ) );
 
+
  my $title = $session->make_text( "Login" );
    # return the value, unchanged
+
  $session->build_page( $title, $page, "login" );
    return $value;     
+
  $session->send_page( %opts );
 +
  $session->terminate;
 +
   return DONE;
 
}
 
}
 
</pre>
 
</pre>
  
====There is a problem====
+
[[Category:Authentication]]
It's not working :sad:
+

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