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.
 
 
==goal==
 
Use an external agency that may define a persistent User ID (a "puid") for someone visiting the repository.
 
 
==explanation==
 
An eprints user may have an identifying puid associated with their account.
 
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.
 
 
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.
 
 
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.
 
''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.
 
 
A puid will be in the format of ''1234:5678#My+Institution''
 
 
==How To Do It==
 
===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)
 
<pre>
 
  {
 
        'name' => 'puid',
 
        'type' => 'text',
 
    },
 
</pre>
 
and the corresponding entries in the workflow and phrases files:
 
 
Workflows (archives/''ARCHIVEID''/cfg/workflows/user/default.xml)
 
<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>
 
phrases (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>
 
'''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===
 
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>
 
######################################################################
 
=pod
 
 
if( $self->get_repository->can_call( 'get_current_user' ) )
 
{
 
  $self->{current_user} = $self->get_repository->call( 'get_current_user', $self );
 
}
 
 
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
 
eprint methods.
 
 
=cut
 
######################################################################
 
$c->{get_current_user} = sub {
 
my( $session ) = @_;
 
 
if( !defined $session->{request} )
 
{
 
# not a cgi script.
 
return undef;
 
}
 
  # check for a eprints session cookie
 
  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;
 
 
  # can we get a user?
 
  my $user = EPrints::DataObj::User::user_with_puid( $session,$puid );
 
  if( defined $user )
 
  {
 
    $session->{via_depot} = 1;
 
  }
 
  return $user;
 
};
 
 
</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>
 
<pre>
 
######################################################################
 
=pod
 
 
=item $puid = $session->puid_from_cookie;
 
 
returns the puid from our own cookie
 
 
Used in both the dynamic template and C<&get_current_user():>
 
 
=cut
 
######################################################################
 
sub EPrints::Session::puid_from_cookie
 
{
 
  my( $self ) = @_;
 
 
  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
 
 
=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>
 
 
and finally <code>$session->{via_depot} = 1</code> just sets a flag in the session.
 
 
===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>
 
######################################################################
 
=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 => 'depot_authen',
 
                  -domain => 'edina.ac.uk',
 
                  -expires => time(),
 
                  -value => '',
 
                  );
 
  EPrints::Apache::AnApache::header_out(
 
$session->{"request"},
 
"Set-Cookie" => $cookie );
 
};
 
 
</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>
 
 
==Allowing the user to change the fields==
 
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. On the other hand, if there is a situation where the user may change puid (hopefully on an infrequent basis), then the user needs to be able to change the puid associated with their account.
 
 
There are three possible senarios for a user logging into the depot:
 
1) The assumed-to-be-default method is via Repository Junction, where the RJ cookie identifies the user, and logs them in automatically
 
2) The user may arrive at http://deposit.depot.edina.ac.uk/ directly, and wish to log in. There is no RJ cookie, so the user enters their login name and password.
 
3) The user comes in via RJ, but has a PUID which is not recognised by the Depot, so they need to login as per method 2 (but noting that there is an RJ cookie set)
 
 
Once logged in, the user may modify their profile.
 
- They may change the email address associated with the account (but never remove it)
 
- They may change their password
 
- They may change (or supply) a name, a department, a postal address, a country, and/or a homepage URL.
 
- The users institution is derived from the users PUID, and alters when the user alters their defined authentication point.
 
 
 
===Changing the way the field is ''rendered''===
 
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>
 
    {
 
        'name'                => 'puid',
 
        'type'                => 'text',
 
        'render_single_value' => \&render_puid,
 
        'render_input'        => \&select_puid,
 
    },
 
</pre>
 
In <code>archives/''ARCHIVEID''/cfg/cfg.d/user_fields.pl</code> we also add the two methods:
 
<pre>
 
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;
 
 
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;
 
 
 
}
 
</pre>
 
==foo==
 
 
This is a real-world example.
 
This is a real-world example.
  

Revision as of 08:55, 21 September 2007

This is a real-world example.

goal

Use an external agency that may define a persistent User ID (a "puid") for someone visiting the repository.

explanation

An eprints user may have an identifying puid associated with their account. 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.

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

A puid will be in the format of 1234:5678#My+Department

How To Do It

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)

   {
        'name' => 'puid',
        'type' => 'text',
    },

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

In EPrints 3.0.3+ you can create a subroutine for an alternative login procedure. Place the following code in archives/ARCHIVEID/cfg/cfg.d/myMethods.pl:

######################################################################
=pod

 if( $self->get_repository->can_call( 'get_current_user' ) )
 {
   $self->{current_user} = $self->get_repository->call( 'get_current_user', $self );
 }

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
eprint methods.

=cut
######################################################################
$c->{get_current_user} = sub {
	my( $session ) = @_;

	if( !defined $session->{request} )
	{
		# not a cgi script.
		return undef;
	}
  # check for a eprints session cookie
  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;

  # can we get a user?
  my $user = EPrints::DataObj::User::user_with_puid( $session,$puid );
  if( defined $user )
  {
	    $session->{via_depot} = 1; 
   }
   return $user;
};

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

######################################################################
=pod

=item $puid = $session->puid_from_cookie;

returns the puid from our own cookie

Used in both the dynamic template and C<&get_current_user():>

=cut
######################################################################
sub EPrints::Session::puid_from_cookie
{
  my( $self ) = @_;

  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

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

and finally $session->{via_depot} = 1 just sets a flag in the session.

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 => 'depot_authen',
                   -domain => 'edina.ac.uk',
                   -expires => time(),
                   -value => '',
                   );
  EPrints::Apache::AnApache::header_out( 
				$session->{"request"},
			"Set-Cookie" => $cookie );
};

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

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

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: 1) The assumed-to-be-default method is via whatever your devolved authentication system, where myCookie identifies the user, and logs them in automatically 2) The user may arrive at your repository directly, and wish to log in. There is no myCookie, so the user enters their login name and password. 3) The user comes in a valid myCookie, but has a PUID which is not recognised by the repository, so they need to login as per method 2 (but noting that myCookie is set)

Once logged in, the user may modify their profile.

Making the field visable in Modify Profile

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

 
}