Skip to content

Commit

Permalink
Add content item selection for LTI.
Browse files Browse the repository at this point in the history
See https://www.imsglobal.org/specs/lticiv1p0/specification
and https://www.imsglobal.org/spec/lti-dl/v2p0.

This will be needed for Moodle 4.03 (and beyond?) but might be nice for
other LMS's as well.  If the new content selection endpoints
(https://your.webwork2.server.domain/ltiadvanced/content_selection for
LTI 1.1 and
https://your.webwork2.server.domain/ltiadvantage/content_selection for
LTI 1.3) are added to the LMS external tool configuration, then
instructors can select from a list of homework assignments in the
webwork2 course, and it will add the correct link(s) to the LMS.  For
Moodle (at least for 4.1 and up) you can add links to all assignments
with a few clicks of the mouse.  Canvas is not quite as nice as it only
allows selection one assignment at a time (at least as far as I have
been able to figure out at this point).  As usual, I am not sure what
D2L can do.

In order for this to work each webwork2 course must set the new LTI
configuration variable `$LMSCourseName` in its course.conf file.  This
is the name of the LMS course that is associated with the webwork2
course.  This must be done because these content item requests do not
have the webwork2 course name in the link like the usual webwork2
course or assignment links.

In order to support multiple LMS's with a single webwork2 server there
is a little more that must be done for LTI 1.1, particularly if there is
the possibility that courses from different LMS's might have the same
LMS course name.  In this case each LMS must use a different consumer
key for its external tool for the webwork2 server, and the
`$LTI{v1p1}{ConsumerKey}` must be set in each courses course.conf file.
For LTI 1.3 no additional configuration is needed because the
authentication parameters already uniquely identify an LMS external
tool.

Note that none of this new configuration is needed in the case that the
webwork2 server administrator does not want instructors to be able to
utilize the new content item selection routes.  The `$LTICourseName`
does not need to be set in the `course.conf` files, the
`$LTI{v1p1}{ConsumerKey}` does not need to be set, and both versions of
LTI will continue to work as they did before.

Now for implementation details.

For LTI 1.1 nothing is changed in the way that authentication works
unless the LMS sends the user to the new `ltiadvanced_content_selection`
route.  In that case the correct course is identified by iterating
through the webwork2 courses on the server, loading the course
environment for each one, and finding the one with the correct LMS
course name (and ConsumerKey if there is more than one with the same LMS
course name).  This is probably rather inefficient, but fortunately this
is only done for content item selection requests.

For LTI 1.3 things needed to be reworked quite a bit more.  All of the
hackery that was previously implemented to pass the course id, state,
and nonce from the login request to the launch request using the key
table had to go.  Instead there is now a non-native table (the
lti_launch_data table) that this information is stored in, identified by
the state generated from the parameters provided by the LMS in the login
request which are unique to the LMS user and request.  This table can be
accessed without a webwork2 course id since it is non-native.    This
allows for the state to be retrieved from the database in the launch
request, and the JWT to be decoded earlier in that request (previously
this was done in the authentication step, and now it is done before that
in the webwork2 dispatch step).  It needs to be done at dispatch time
because the JWT contains the information that determines if it is a deep
linking request or not.  For both the login and launch requests a
webwork2 course is identified by iterating through courses, loading
course environments, and finding an LTI 1.3 tool configuration that
matches that in the request.  Again this is probably rather inefficient,
but again this is only done for content item selection requests.

There is a new hook that can be utilized by content generator modules to
modify parameters, route captures, and do other things early in the
dispatch phase before a course id has been determined and a course
environment and database instance is initialized. The hook can be
utilized by the content generator module providing an `initializeRoute`
method.  This is just clean up really.  A lot of route specific special
cases were starting to build up in the `dispatch` method in
`lib/WeBWork.pm`, and this makes it possible for that code to be moved
out of there.
  • Loading branch information
drgrice1 committed Feb 13, 2024
1 parent 75b40e2 commit 733de0f
Show file tree
Hide file tree
Showing 16 changed files with 793 additions and 294 deletions.
37 changes: 22 additions & 15 deletions conf/authen_LTI.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,33 @@
# Configuration for using LTI authentication.
# To enable this file, uncomment the appropriate lines in localOverrides.conf
# The settings in this file apply to both LTI 1.1 and LTI 1.3 authentication.
# The settings specific to the LTI 1.1 authenticatio are in authen_LTI_1_1.conf.
# The settings specific to the LTI 1.3 authenticatio are in authen_LTI_1_3.conf.
# The settings specific to the LTI 1.1 authentication are in authen_LTI_1_1.conf.
# The settings specific to the LTI 1.3 authentication are in authen_LTI_1_3.conf.
################################################################################################

# Set debug_lti_parameters to 1 to have LTI calling parameters printed to HTML page for
# debugging. This is useful when setting things up for the first time because different LMS
# systems have different parameters
# Set debug_lti_parameters to 1 to enable LTI debugging. This is useful when setting things up
# for the first time because different LMS systems have different parameters. Note that for LTI
# 1.1 these debug messages will be displayed in the HTML page. However, for LTI 1.3 none of the
# debug messages will be displayed in the HTML page due to the nature of how LTI 1.3
# authentication works with automatic form submissions and redirects. These messages can be
# found in the webwork2 app log in that case.
$debug_lti_parameters = 0;

# To get more information on passing grades back to the LMS enmass set debug_lti_grade_passback
# To get more information on passing grades back to the LMS en mass set debug_lti_grade_passback
# to one. And set the LTIMassUpdateInterval to 60 (seconds).
$debug_lti_grade_passback = 0;

# This will print into the webwork2 app log the success or failure of updating each user/set.

# Setting both debug_lti_parameters and debug_lti_grade_passback will cause the full request and
# response between the LMS and WW to be printed into webwork2 app log file for each user/set
# update of the grade.
# Setting both debug_lti_parameters and debug_lti_grade_passback will cause the full requests
# and responses between the LMS and WW to be printed into webwork2 app log file for each
# user/set update of the grade.

# The switches above can be set in course.conf to enable debugging for just one course.

# If you want even more information enable the debug facility in the webwork2.mojolicious.yml
# file. This will print extensive debugging messages for all courses.

# Note that for LTI 1.3 not all debug message will make it back to the HTML page due to the
# nature of how LTI 1.3 authentication works with automatic form submissions and redirects.

################################################################################################
# Authentication settings
################################################################################################
Expand Down Expand Up @@ -58,6 +58,13 @@ $authen{user_module} = [
# disable LTI authentication for the course.
$LTIVersion = 'v1p1';

# The $LMSCourseName is the name of the course in the LMS that uses a particular webwork2 course
# as a tool provider. This should never be set in this file, but rather should be set in each
# webwork2 course's course.conf file. This only needs to be set in the course.conf files for
# each course if you want to allow the instructors of courses on this server to utilize content
# item selection requests.
#$LMSCourseName = 'LMS Course Title';

# WeBWorK will automatically create users when logging in via the LMS for the first time as long
# as the permission level is less than or equal to the permission level of this setting. For
# security reasons accounts with high permissions should not be auto created via LTI requests.
Expand Down Expand Up @@ -104,7 +111,7 @@ $external_auth = 0;
# address should be the address of that set in the Course. Students will receive a grade for
# each Link/Assignment which is determined by their percentage homework grade on the Set which
# the Link/Assignment points to. Students need to use the Link/Assignment in the LMS at least
# once to enable grade passback. In particular when working in this mode it is recommended that
# once to enable grade pass back. In particular when working in this mode it is recommended that
# you only allow students to log in via the LMS.

# Note: For both of these modes only the grades are passed back. In particular nothing else
Expand All @@ -115,7 +122,7 @@ $external_auth = 0;
# Site Administrator Note for LTI 1.3: This uses OAuth2 RSA private/public keys. These keys
# are automatically generated the first time that they are needed. It is recommended that new
# keys are generated on a regular basis. At this point, key rotation is not automatic for
# webwork2. Howevever, it is simple. Delete the files $webwork2_dir/DATA/lti_private_key.json
# webwork2. However, it is simple. Delete the files $webwork2_dir/DATA/lti_private_key.json
# and $webwork2_dir/DATA/lti_public_key.json. New keys will then be automatically generated the
# next time they are needed. Probably a good rule of thumb (for now) is to do this at the
# beginning of every term.
Expand All @@ -128,7 +135,7 @@ $LTIGradeMode = '';
# keeps students grades up to date but can be a drain on the server.
$LTIGradeOnSubmit = 1;

# If CheckPrior is set to 1 then the current LMS grade will be checked first, and if the grade
# If $LTICheckPrior is set to 1 then the current LMS grade will be checked first, and if the grade
# has not changed then the grade will not be updated. This is intended to reduce changes to LMS
# records when no real grade change occurred. It requires a 2 round process, first querying the
# current grade from the LMS and then when needed making the grade submission.
Expand Down
12 changes: 12 additions & 0 deletions conf/authen_LTI_1_1.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ $LTI{v1p1}{preferred_source_of_student_id} = '';
# You should choose your own secret word for security and should treat it like a password.
$LTI{v1p1}{BasicConsumerSecret} = '';

# The consumer key is entered in the LMS request form, and needs to match the entry here if this
# is set. This is only used for content item selection requests from an LMS, and this is does
# not even need to be set for that unless there are multiple courses from different LMS's that
# have the same LMS course name and both use a course on this webwork2 server. In that case
# each LMS must use a different consumer key, and the correct consumer keys should be set in the
# course.conf file for each course. If this server is a tool provider for multiple LMS's, then
# it is recommended that this be set. Usually it is not useful to set this here. However, if
# most courses use one LMS and only a few use another, then the consumer key for the first LMS
# could be set here, and then this would only need to be set in the course.conf files for the
# few courses use a different LMS.
#$LTI{v1p1}{ConsumerKey} = 'webwork';

# The purpose of the LTI nonces is to prevent man-in-the-middle attacks. The NonceLifeTime (in
# seconds) must be short enough to prevent at least casual man-in-the-middle attacks but long
# enough to accommodate normal server and networking delays (and perhaps non-synchronization of
Expand Down
9 changes: 9 additions & 0 deletions conf/database.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ $dbLayouts{sql_single} = {
engine => $database_storage_engine,
params => { %sqlParams, non_native => 1 },
},
lti_launch_data => {
record => "WeBWorK::DB::Record::LTILaunchData",
schema => "WeBWorK::DB::Schema::NewSQL::Std",
driver => "WeBWorK::DB::Driver::SQL",
source => $database_dsn,
engine => $database_storage_engine,
character_set => $database_character_set,
params => { %sqlParams, non_native => 1 },
},
password => {
record => "WeBWorK::DB::Record::Password",
schema => "WeBWorK::DB::Schema::NewSQL::Std",
Expand Down
50 changes: 12 additions & 38 deletions lib/WeBWorK.pm
Original file line number Diff line number Diff line change
Expand Up @@ -87,42 +87,6 @@ async sub dispatch ($c) {
my $displayModule = ref $c;
my %routeCaptures = %{ $c->stash->{'mojo.captures'} };
if ($c->current_route =~ /^(render_rpc|instructor_rpc|html2xml)$/) {
$c->{rpc} = 1;
# This provides compatibility for legacy html2xml parameters.
# This should be deleted when the html2xml endpoint is removed.
if ($c->current_route eq 'html2xml') {
for ([ 'userID', 'user' ], [ 'course_password', 'passwd' ], [ 'session_key', 'key' ]) {
$c->param($_->[1], $c->param($_->[0])) if defined $c->param($_->[0]) && !defined $c->param($_->[1]);
}
}
# Get the courseID from the parameters for a remote procedure call.
$routeCaptures{courseID} = $c->param('courseID') if $c->param('courseID');
}
# If this is the login phase of an LTI 1.3 login, then extract the courseID from the target_link_uri.
if ($c->current_route eq 'ltiadvantage_login') {
my $target = $c->param('target_link_uri') ? $c->url_for($c->param('target_link_uri'))->path : '';
$c->stash->{courseID} = $1 if $target =~ m|$location/([^/]*)|;
$routeCaptures{courseID} = $c->stash->{courseID} || '___';
}
# If this is the launch phase of an LTI 1.3 login, then get the courseID from the state. Note that this data can
# not be trusted. It will be verified once the JWT is decrypted. Also see the comments about the hacks involved
# here in the WeBWorK::Authen::LTIAdvantage::verify method.
if ($c->current_route eq 'ltiadvantage_launch') {
$c->stash->{LTIState} = $c->param('state') || ',set_id:';
($c->stash->{lti_lms_user_id}, $c->stash->{courseID}) = split ',set_id:', $c->stash->{LTIState};
if ($c->stash->{courseID} && $c->stash->{lti_lms_user_id}) {
$c->stash->{courseID} =~ s/@/-/g;
$routeCaptures{courseID} = $c->stash->{courseID};
} else {
$routeCaptures{courseID} = '___';
}
}
debug("The display module for this route is $displayModule\n");
debug("This route has the following captures:\n");
for my $key (keys %routeCaptures) {
Expand Down Expand Up @@ -153,6 +117,9 @@ async sub dispatch ($c) {
debug(('-' x 80) . "\n");
# A controller can customize route captures, parameters, and stash values if it provides an initializeRoute method.
$c->initializeRoute(\%routeCaptures) if $c->can('initializeRoute');
# Create Course Environment
debug("We need to get a course environment (with or without a courseID!)\n");
my $ce = eval { WeBWorK::CourseEnvironment->new({ courseName => $routeCaptures{courseID} }) };
Expand Down Expand Up @@ -196,9 +163,11 @@ async sub dispatch ($c) {
debug("We got a courseID from the route, now we can do some stuff:\n");
return (0, 'This course does not exist.')
unless (-e $ce->{courseDirs}{root}
unless ($routeCaptures{courseID} eq '___'
|| -e $ce->{courseDirs}{root}
|| -e "$ce->{webwork_courses_dir}/$ce->{admin_course_id}/archives/$routeCaptures{courseID}.tar.gz");
return (0, 'This course has been archived and closed.') unless -e $ce->{courseDirs}{root};
return (0, 'This course has been archived and closed.')
unless $routeCaptures{courseID} eq '___' || -e $ce->{courseDirs}{root};
debug("...we can create a database object...\n");
my $db = WeBWorK::DB->new($ce->{dbLayout});
Expand Down Expand Up @@ -266,6 +235,11 @@ async sub dispatch ($c) {
await WeBWorK::ContentGenerator::Login->new($c)->go();
return 0;
}
} else {
return (0,
'No WeBWorK course was found associated to this LMS course. '
. 'If this is an error, please contact the WeBWorK system administrator.')
if $c->current_route eq 'ltiadvantage_login';
}
return 1;
Expand Down
Loading

0 comments on commit 733de0f

Please sign in to comment.