Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix (and completely revamp) the Shibboleth authentication module. (hotfix of #2611) #2612

Merged
merged 3 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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