Skip to content

Commit

Permalink
Fix (and completely revamp) the Shibboleth authentication module.
Browse files Browse the repository at this point in the history
This updates the Shibboleth authentication module to fit into the new
scheme of the general webwork2 authentication process.  The module is
set up to work just like all of the other up to date webwork2
authentication modules.

It has its own configuration file (conf/authen_shibboleth.conf.dist)
that should be used instead of adding a buch of variables to
localOverrides.conf.  The include statement in localOverrides.conf
should be uncommented, and the dist file copied and modified.
Furthermore, relatively complete instructions on how to use the
authentication module are in the comments in the configuration file.

The variables in the configuration file are all the same as before,
except that there is one new one.  That is the
`$shibboleth{bypass_query}`.  Previously "bypassShib" was hard coded for
this purpose.  Now that can be configured.  If that variable is not set
(and for those using this module before it wouldn't be), then the bypass
parameter will not work.  So this is the only real change from before.

The issues that were causing webwork2's session not to work have been
fixed.  This means that proctored test access will work again.

The library browser, pg problem editor, and everything else that uses
the rpc endpoints will work correctly.  There simply is nothing special
that the authentication module needs to do here, and most importantly it
needs to not do anything special (like reverting to the base
authentication module).  The rpc enpoints now use the usual
authentication methods, and that does work with mod_shib.
  • Loading branch information
drgrice1 committed Oct 25, 2024
1 parent c4b8039 commit 1f1224a
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 123 deletions.
143 changes: 143 additions & 0 deletions conf/authen_shibboleth.conf.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!perl
################################################################################
# Configuration for using Shibboleth authentication.
#
# To enable Shibboleth authentication, copy this file to
# conf/authen_shibboleth.conf and uncomment the appropriate lines in
# localOverrides.conf.
#
################################################################################

# Note that Shibboleth authentication will only work if webwork2 is proxied via
# apache2 and a Shibboleth service provider (mod_shib) is installed and
# configured. Instructions on how to configure the Shibboleth service provider
# are below. These instructions are specifically for Ubuntu, and setup will be
# slightly different on other systems.
#
# Install the Shibboleth service provider for apache2 by installing the Ubuntu
# apache2-mod-shib package.
#
# Modify the /etc/shibboleth/shibboleth2.xml file as follows.
#
# Change the "entityID" attribute of the "ApplicationDefaults" tag to
# https://your.server.edu`. Note that the Shibboleth identity provider that you
# will use will also need to be configured to allow this "entityID" to work with
# it.
#
# Change the "SSO" tag in the "Sessions" section to
# <SSO entityID="https://your.idp.server/url/for/metadata">SAML2</SSO>
#
# Near the end of the file where example "MetadataProvider" sections are
# located add the following "MetadataProvider" tag.
# <MetadataProvider
# type="XML"
# url="https://your.idp.server/url/for/metadata"
# backingFilePath="idp-metadata.xml"
# maxRefreshDelay="7200"
# />
#
# Note that further adjustments to that file may be needed depending on how your
# Shibboleth identity provider is set up.
#
# Next modify the /etc/shibboleth/attribute-map.xml file by adding the attribute
# that will be used for $shibboleth{mapping}{user_id} below. For example, if
# you are using the "uid" as in the default value of that variable, then add
# <Attribute name="urn:oid:0.9.2342.19200300.100.1.1" id="uid"/>
# to the Attributes section of the file.
# Note the file already has some attributes configured, and so you may not need
# to modify that file at all. For example, if you use "eppn" for
# $shibboleth{mapping}{user_id}, then you don't need to change that file, since
# "eppn" is already listed.
#
# Finally, configure apache2 to protect route webwork2 course URLs to the
# Shibboleth service provider by adding one of the following to your apache2
# site configuration file.
#
# <LocationMatch ^/webwork2/.+>
# AuthType shibboleth
# ShibRequestSetting requireSession 1
# Require valid-user
# RequestHeader unset uid
# RequestHeader set uid %{uid}e env=uid
# </LocationMatch>
#
# or
#
# <LocationMatch ^/webwork2/.+>
# AuthType shibboleth
# ShibRequestSetting requireSession 0
# Require shibboleth
# RequestHeader unset uid
# RequestHeader set uid %{uid}e env=uid
# </LocationMatch>
#
# Use the first if you want strict Shibboleth authentication. With this set up
# the webwork2 app will never see course URL requests if the user is not first
# authenticated with the Shibboleth identity provider. The apache2 Shibboleth
# service provider module will redirect the user first.
#
# Use the second if you want lazy Shibboleth authentication. With this set up
# the course URL requests will continue to the webwork2 app even if the user has
# not authenticated with the Shibboleth identity provider. The webwork2 app will
# redirect the user if authentication is needed. This allows for the usage of
# the $shibboleth{bypass_query} parameter or the $shiboff option described
# below.
#
# In both cases change all instances of "uid" to whatever you are using for the
# value of $shibboleth{mapping}{user_id} below.
#
# Execute "sudo shibd -t" to test the Shibboleth service provider configuration.
# Make sure to execute "sudo systemctl restart apache2" and
# "sudo systemctl restart shibd" so that settings take effect.

################################################################################

# Set Shibboleth as the authentication module to use.
# Comment out 'WeBWorK::Authen::Basic_TheLastOption' if bypassing Saml2
# authentication via the bypass query option (see $shibboleth{bypass_query}
# below) or the $shiboff option are both not allowed .
$authen{user_module} = [
'WeBWorK::Authen::Shibboleth',
'WeBWorK::Authen::Basic_TheLastOption'
];

# List of authentication modules that may be used to enter the admin course.
# This is used instead of $authen{user_module} when logging into the admin
# course. Since the admin course provides overall power to add/delete courses,
# access to this course should be protected by the best possible authentication
# you have available to you.
$authen{admin_module} = [
'WeBWorK::Authen::Shibboleth'
];

# Set $shiboff to 1 to disable Shibboleth authentication. Usually this is not
# set here, but in the course.conf file for a course for which Shibboleth
# authentication is to be disabled.
#$shiboff = 0;

# This URL query parameter can be added to the end of a course URL to skip the
# Shibboleth authentication module and go to the next one, for example,
# http://your.school.edu/webwork2/courseID?bypassShib=1. Comment out the next
# line to disable this feature.
$shibboleth{bypass_query} = 'bypassShib';

# The Shibboleth service provider login path.
$shibboleth{login_script} = '/Shibboleth.sso/Login';

# The Shibboleth service provider logout path. The default setting below
# demonstrates how to have the user redirected back to the course login page
# after the logout is complete.
$shibboleth{logout_script} = '/Shibboleth.sso/Logout?return=' . $server_root_url . $webwork_url;

# Set to 1 to allow Shibboleth to manage session time instead of webwork.
$shibboleth{manage_session_timeout} = 1;

# The user id hash method. The possible values are 'none' or 'MD5'. Use it when
# you want to hide real user_ids from showing in the URL.
$shibboleth{hash_user_id_method} = 'none';

# The salt to use for the hash method.
$shibboleth{hash_user_id_salt} = '';

# Set to the Shibboleth attribute that will be used for the webwork user id.
$shibboleth{mapping}{user_id} = 'uid';
10 changes: 10 additions & 0 deletions conf/localOverrides.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,16 @@ $mail{feedbackRecipients} = [

#include("conf/authen_ldap.conf");

################################################################################
# Shibboleth Authentication
################################################################################

# Uncomment the following line to enable Shibboleth authentication. You will
# also need to copy the file authen_shibboleth.conf.dist to authen_shibboleth.conf,
# and then edit that file to fill in the settings for your installation.

#include("conf/authen_shibboleth.conf");

################################################################################
# Session Management
################################################################################
Expand Down
184 changes: 62 additions & 122 deletions lib/WeBWorK/Authen/Shibboleth.pm
Original file line number Diff line number Diff line change
Expand Up @@ -14,175 +14,115 @@
################################################################################

package WeBWorK::Authen::Shibboleth;
use base qw/WeBWorK::Authen/;
use Mojo::Base 'WeBWorK::Authen', -signatures;

=head1 NAME
WeBWorK::Authen::Shibboleth - Authentication plug in for Shibboleth.
This is basd on Cosign.pm
For documentation, please refer to http://webwork.maa.org/wiki/External_(Shibboleth)_Authentication
=head1 SYNOPSIS
to use: include in localOverrides.conf or course.conf
$authen{user_module} = "WeBWorK::Authen::Shibboleth";
and add /webwork2/courseName as a Shibboleth Protected
Location or enable lazy session.
To use this module copy C<conf/authen_shibboleth.conf.dist> to
C<conf/authen_shibboleth.dist>, and uncomment the line in C<conf/localOverrides.conf>
that reads C<include("conf/authen_shibboleth.conf");>.
if $c->ce->{shiboff} is set for a course, authentication reverts
to standard WeBWorK authentication.
add the following to localOverrides.conf to setup the Shibboleth
$shibboleth{login_script} = "/Shibboleth.sso/Login"; # login handler
$shibboleth{logout_script} = "/Shibboleth.sso/Logout?return=".$server_root_url.$webwork_url; # return URL after logout
$shibboleth{manage_session_timeout} = 1; # allow shib to manage session time instead of webwork
$shibboleth{hash_user_id_method} = "MD5"; # possible values none, MD5. Use it when you want to hide real user_ids from showing in url.
$shibboleth{hash_user_id_salt} = ""; # salt for hash function
#define mapping between shib and webwork
$shibboleth{mapping}{user_id} = "username";
Refer to the L<external Shibboleth authentication|http://webwork.maa.org/wiki/External_(Shibboleth)_Authentication>
documentation on the WeBWorK wiki and the instructions in the comments of the
C<conf/authen_shibboleth.conf.dist> file.
=cut

use strict;
use warnings;

use WeBWorK::Debug;
use Digest;

# this is similar to the method in the base class, except that Shibboleth
# ensures that we don't get to the address without a login. this means
# that we can't allow guest logins, but don't have to do any password
# checking or cookie management.
use WeBWorK::Debug qw(debug);

sub get_credentials {
my ($self) = @_;
my $c = $self->{c};
my $ce = $c->ce;
my $db = $c->db;
sub request_has_data_for_this_verification_module ($self) {
my $c = $self->{c};

if ($ce->{shiboff} || $c->param('bypassShib')) {
return $self->SUPER::get_credentials(@_);
# Skip if shiboff is set in the course environment or the bypassShib param is set.
if ($c->ce->{shiboff} || ($c->ce->{shibboleth}{bypass_query} && $c->param($c->ce->{shibboleth}{bypass_query}))) {
debug('Shibboleth authen module bypass detected. Going to next authentication module.');
return 0;
}

$c->stash(disable_cookies => 1);
return 1;
}

debug("Shib is on!");
sub get_credentials ($self) {
my $c = $self->{c};
my $ce = $c->ce;
my $db = $c->db;

# set external auth parameter so that Login.pm knows
# not to rely on internal logins if there's a check_user
# failure.
$c->stash(disable_cookies => 1);
$self->{external_auth} = 1;

if ($c->param("user") && !$c->param("force_passwd_authen")) {
return $self->SUPER::get_credentials(@_);
}
debug('Checking for shibboleth authentication headers.');

# This next part is necessary because some parts of webwork (e.g.,
# WebworkWebservice.pm) need to replace the get_credentials() routine,
# but only replace the one in the parent class (out of caution,
# presumably). Therefore, we end up here even when authenticating
# for WebworkWebservice.pm. This would cause authentication failures
# when authenticating javascript web service requests (e.g., the
# Library Browser).

if ($c->{rpc}) {
debug("falling back to superclass get_credentials (rpc call)");
return $self->SUPER::get_credentials(@_);
}
my $user_id;
$user_id = $c->req->headers->header($ce->{shibboleth}{mapping}{user_id}) if $ce->{shibboleth}{mapping}{user_id};

my $user_id = "";
my $shib_header = $ce->{shibboleth}{mapping}{user_id};
if (defined $user_id && $user_id ne '') {
debug("Got shibboleth header ($ce->{shibboleth}{mapping}{user_id}) and user_id ($user_id)");

if ($shib_header ne "") {
$user_id = $c->req->headers->header($shib_header);
}

if ($user_id ne "") {
debug("Got shib header ($shib_header) and user_id ($user_id)");
if (defined($ce->{shibboleth}{hash_user_id_method})
&& $ce->{shibboleth}{hash_user_id_method} ne "none"
&& $ce->{shibboleth}{hash_user_id_method} ne "")
&& $ce->{shibboleth}{hash_user_id_method} ne 'none'
&& $ce->{shibboleth}{hash_user_id_method} ne '')
{
use Digest;
my $digest = Digest->new($ce->{shibboleth}{hash_user_id_method});
$digest->add(
uc($user_id)
. (defined $ce->{shibboleth}{hash_user_id_salt} ? $ce->{shibboleth}{hash_user_id_salt} : ""));
$digest->add(uc($user_id) . ($ce->{shibboleth}{hash_user_id_salt} // ''));
$user_id = $digest->hexdigest;
}
$self->{'user_id'} = $user_id;
$self->{c}->param("user", $user_id);

# the session key isn't used (Shibboleth is managing this
# for us), and we want to force checking against the
# site_checkPassword
$self->{'session_key'} = undef;
$self->{'password'} = 1;
$self->{login_type} = "normal";
$self->{'credential_source'} = "params";
$self->{user_id} = $user_id;
$c->param('user', $user_id);
$self->{login_type} = 'normal';
$self->{credential_source} = 'params';

return 1;
}

debug("Couldn't shib header or user_id");
my $go_to = $ce->{shibboleth}{login_script} . "?target=" . $c->url_for->to_abs;
$self->{redirect} = $go_to;
$c->redirect_to($go_to);
debug('Unable to obtain user id from Shibboleth header.');
$self->{redirect} = $ce->{shibboleth}{login_script} . '?target=' . $c->url_for->to_abs;
$c->redirect_to($self->{redirect});
return 0;
}

sub site_checkPassword {
my ($self, $userID, $clearTextPassword) = @_;

if ($self->{c}->ce->{shiboff} || $self->{c}->param('bypassShib')) {
return $self->SUPER::checkPassword(@_);
} else {
# this is easy; if we're here at all, we've authenticated
# through shib
return 1;
}
sub authenticate ($self) {
# The Shibboleth identity provider handles authentication, so just return 1.
return 1;
}

# this is a bit of a cheat, because it does the redirect away from the
# logout script or what have you, but I don't see a way around that.
sub forget_verification {
my ($self, @args) = @_;
my $c = $self->{c};

if ($c->ce->{shiboff}) {
return $self->SUPER::forget_verification(@_);
} else {
$self->{was_verified} = 0;
$self->{redirect} = $c->ce->{shibboleth}{logout_script};
}
sub logout_user ($self) {
$self->{redirect} = $self->{c}->ce->{shibboleth}{logout_script};
return;
}

# returns ($sessionExists, $keyMatches, $timestampValid)
# if $updateTimestamp is true, the timestamp on a valid session is updated
# override function: allow shib to handle the session time out
sub check_session {
my ($self, $userID, $possibleKey, $updateTimestamp) = @_;
sub check_session ($self, $userID, $possibleKey, $updateTimestamp) {
my $ce = $self->{c}->ce;
my $db = $self->{c}->db;

if ($ce->{shiboff}) {
return $self->SUPER::check_session(@_);
} else {
my $Key = $db->getKey($userID); # checked
return 0 unless defined $Key;

my $keyMatches = (defined $possibleKey and $possibleKey eq $Key->key);
my $timestampValid = (time <= $Key->timestamp() + $ce->{sessionTimeout});
if ($ce->{shibboleth}{manage_session_timeout}) {
# always valid to allow shib to take control of timeout
$timestampValid = 1;
}
my $Key = $db->getKey($userID);
return 0 unless defined $Key;

if ($keyMatches and $timestampValid and $updateTimestamp) {
$Key->timestamp(time);
$db->putKey($Key);
}
return (1, $keyMatches, $timestampValid);
# This is filled in just in case it is needed somewhere, but is not used in the Shibboleth authentication process.
$self->{session_key} = $Key->{key};

my $currentTime = time;
my $timestampValid =
$ce->{shibboleth}{manage_session_timeout} ? 1 : time <= $Key->timestamp + $ce->{sessionTimeout};

if ($timestampValid && $updateTimestamp) {
$Key->timestamp($currentTime);
$self->{c}->stash->{'webwork2.database_session'} = { $Key->toHash };
$self->{c}->stash->{'webwork2.database_session'}{session}{flash} =
delete $self->{c}->stash->{'webwork2.database_session'}{session}{new_flash}
if $self->{c}->stash->{'webwork2.database_session'}{session}{new_flash};
}

return (1, 1, $timestampValid);
}

1;
Loading

0 comments on commit 1f1224a

Please sign in to comment.