From a17feee3af5374e7b9f7a655741529c8a5bba1fe Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Thu, 24 Oct 2024 05:21:54 -0500 Subject: [PATCH 1/3] Fix (and completely revamp) the Shibboleth authentication module. 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. --- conf/authen_shibboleth.conf.dist | 143 ++++++++++++++++++++++++ conf/localOverrides.conf.dist | 10 ++ lib/WeBWorK/Authen/Shibboleth.pm | 184 +++++++++++-------------------- lib/WeBWorK/Controller.pm | 2 +- 4 files changed, 216 insertions(+), 123 deletions(-) create mode 100644 conf/authen_shibboleth.conf.dist diff --git a/conf/authen_shibboleth.conf.dist b/conf/authen_shibboleth.conf.dist new file mode 100644 index 0000000000..7453144c89 --- /dev/null +++ b/conf/authen_shibboleth.conf.dist @@ -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 +# SAML2 +# +# Near the end of the file where example "MetadataProvider" sections are +# located add the following "MetadataProvider" tag. +# +# +# 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 +# +# 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. +# +# +# AuthType shibboleth +# ShibRequestSetting requireSession 1 +# Require valid-user +# RequestHeader unset uid +# RequestHeader set uid %{uid}e env=uid +# +# +# or +# +# +# AuthType shibboleth +# ShibRequestSetting requireSession 0 +# Require shibboleth +# RequestHeader unset uid +# RequestHeader set uid %{uid}e env=uid +# +# +# 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'; diff --git a/conf/localOverrides.conf.dist b/conf/localOverrides.conf.dist index 39b4a02afe..555f2f0f91 100644 --- a/conf/localOverrides.conf.dist +++ b/conf/localOverrides.conf.dist @@ -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 ################################################################################ diff --git a/lib/WeBWorK/Authen/Shibboleth.pm b/lib/WeBWorK/Authen/Shibboleth.pm index d29cc86707..a762a13aca 100644 --- a/lib/WeBWorK/Authen/Shibboleth.pm +++ b/lib/WeBWorK/Authen/Shibboleth.pm @@ -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 to +C, and uncomment the line in C +that reads C. -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 +documentation on the WeBWorK wiki and the instructions in the comments of the +C 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; diff --git a/lib/WeBWorK/Controller.pm b/lib/WeBWorK/Controller.pm index 6e2f36cf08..465e326184 100644 --- a/lib/WeBWorK/Controller.pm +++ b/lib/WeBWorK/Controller.pm @@ -63,7 +63,7 @@ sub param ($c, @opts) { # Override the Mojolicious::Controller session method to set the cookie parameters # from the course environment the first time it is called. sub session ($c, @args) { - return if $c->stash('disable_cookies'); + return {} if $c->stash('disable_cookies'); # Initialize the cookie session the first time this is called. unless ($c->stash->{'webwork2.cookie_session_initialized'}) { From 43b8295db79fe475ac75cd17d11ac890497635dc Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Mon, 28 Oct 2024 14:05:44 -0500 Subject: [PATCH 2/3] Remove `use strict` and `use warnings` from the Shibboleth.pm module. Don't worry. The module is still strict and has warnings enabled. All modules that derive from `Mojo::Base` are. I forgot to remove these when changing to that, and those cause warnings since Mojo::Base disables the warnings for using signatures. --- lib/WeBWorK/Authen/Shibboleth.pm | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/WeBWorK/Authen/Shibboleth.pm b/lib/WeBWorK/Authen/Shibboleth.pm index a762a13aca..d0d1d419cb 100644 --- a/lib/WeBWorK/Authen/Shibboleth.pm +++ b/lib/WeBWorK/Authen/Shibboleth.pm @@ -32,9 +32,6 @@ C file. =cut -use strict; -use warnings; - use Digest; use WeBWorK::Debug qw(debug); From 975a9da4df07ebf3aa141baa022813dd355459af Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Mon, 28 Oct 2024 19:50:55 -0500 Subject: [PATCH 3/3] Make the Shibboleth bypass_query parameter actually work. Currently the parameter works to sign in, but if you try to do anything after signing in, then you are redirected to sign in to the Shibboleth identity provider. To prevent that the parameter needs to be considered a persistent authentication parameter. --- lib/WeBWorK/ContentGenerator.pm | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/WeBWorK/ContentGenerator.pm b/lib/WeBWorK/ContentGenerator.pm index baa9f5a24e..9b9cb538e7 100644 --- a/lib/WeBWorK/ContentGenerator.pm +++ b/lib/WeBWorK/ContentGenerator.pm @@ -1070,6 +1070,10 @@ session_management_via is "key" then the "key" is added. sub hidden_authen_fields ($c, $id_prefix = undef) { my @fields = ('user', 'effectiveUser'); push(@fields, 'key') if $c->ce->{session_management_via} ne 'session_cookie'; + + # Make the Shibboleth bypass_query parameter persistent if it is configured. + push(@fields, $c->ce->{shibboleth}{bypass_query}) if $c->ce->{shibboleth}{bypass_query}; + return $c->hidden_fields({ id_prefix => $id_prefix }, @fields) if defined $id_prefix; return $c->hidden_fields(@fields); } @@ -1106,10 +1110,11 @@ sub url_authen_args ($c) { # When cookie based session management is in use, there should be no need # to reveal the user and key in the URL. Putting it there makes session # hijacking easier, in particular should a student share such a URL. + # If the Shibboleth authentication module is in use, then make the bypass_query parameter persistent. if ($ce->{session_management_via} eq 'session_cookie') { - return $c->url_args('effectiveUser'); + return $c->url_args('effectiveUser', $c->ce->{shibboleth}{bypass_query} // ()); } else { - return $c->url_args('user', 'effectiveUser', 'key'); + return $c->url_args('user', 'effectiveUser', 'key', $c->ce->{shibboleth}{bypass_query} // ()); } } @@ -1188,6 +1193,9 @@ sub systemLink ($c, $urlpath, %options) { } $params{effectiveUser} = undef unless exists $params{effectiveUser}; + + # Make the Shibboleth bypass_query parameter persistent if it is configured. + $params{ $c->ce->{shibboleth}{bypass_query} } = undef if $c->ce->{shibboleth}{bypass_query}; } my $url = $options{use_abs_url} ? $urlpath->to_abs : $urlpath;