Skip to content

Commit

Permalink
Merge pull request openwebwork#2612 from drgrice1/shibboleth-rework-h…
Browse files Browse the repository at this point in the history
…otfix

Fix (and completely revamp) the Shibboleth authentication module.  (hotfix of openwebwork#2611)
  • Loading branch information
Alex-Jordan authored Nov 13, 2024
2 parents beda372 + 975a9da commit c37af2b
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 128 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
187 changes: 62 additions & 125 deletions lib/WeBWorK/Authen/Shibboleth.pm
Original file line number Diff line number Diff line change
Expand Up @@ -14,175 +14,112 @@
################################################################################

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 ($shib_header ne "") {
$user_id = $c->req->headers->header($shib_header);
}
if (defined $user_id && $user_id ne '') {
debug("Got shibboleth header ($ce->{shibboleth}{mapping}{user_id}) and user_id ($user_id)");

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 c37af2b

Please sign in to comment.