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..d0d1d419cb 100644 --- a/lib/WeBWorK/Authen/Shibboleth.pm +++ b/lib/WeBWorK/Authen/Shibboleth.pm @@ -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 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 ($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; 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; 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'}) {