Skip to content

Commit ddb0074

Browse files
committed
Implement LTI 1.3 dynamaic registration with the LMS.
This implements the specification detailed at https://www.imsglobal.org/spec/lti-dr/v1p0. To use this the LMS administrator enters the URL `https://your.webwork2.server.edu/webwork2/ltiadvantage/registration`. That automatically adds the LTI 1.3 configuration for the webwork2 server to the LMS. Then the LMS administrator just needs to activate the tool. On the webwork2 side of things the LTI 1.3 configuration for the LMS will be saved into a file in the directory `webwork/DATA/LTIRegistrationRequests`. The file will be named `$lmsName-XXXX.conf` where `$lmsName` is whatever the tool reported in the `product_family_code` subkey of the `https://purl.imsglobal.org/spec/lti-platform-configuration` key in the configuration webwork2 obtains from the LMS and `XXXX` is whatever `tempfile` fills in to ensure the file is unique. Note that the `product_family_code` is "moodle", "canvas", etc. Unfortunately, there isn't really a unique identifier that can be used here in the information sent from the LMS, so not much better can be done for the file name. The webwork2 system administrator then needs to copy and paste the contents of that file into either the `conf/authen_LTI_1_3.conf` file for site wide setup, or into all of the appropriate `course.conf` files for course specific setup. Note that depending on the LMS the data in the file may not be complete. The specification essentially states that it is optional for the LMS to sent this value. Moodle sends the `deployment_id` in the returned configuration, but Canvas does not. Of course I don't know what D2L or Blackboard will do. In this case the generated will contain `'obtain from LMS administrator`' for the `$LTI{v1p3}{DeploymentID}`. So for Canvas, at least, the webwork2 administrator will still need to communicate with the LMS administrator to obtain the `deployment_id`. Eventually, a user interface in the admin course could perhaps be implemented for dealing with these configurations in a nicer way than cutting a pasting from this file that is created. However, that most likely will require a change in how the LTI configurations are saved. The config file approach is a limiting factor in this. Also, there may be additional configuration that the LMS administrator needs (or may want) to do, and how the tool is presented in the LMS when editing it may be different than how it was previously with the manual configuration approach. For Moodle the tool that is automatically created needs to be activated (a click of a button does this), but furthermore, the administrator will probably want to edit the configuration and set the "Tool configuration usage" to "Show in activity chooser and as preconfigured tool" (it is set to "Show as preconfigured tool when adding an external tool" by default), and set "Default launch container" to "New Window" (it is set to "Embed, without blocks" by default). Also, the way the tool is presented when editing it is indistinguishable from a tool created using the manual configuration approach. This means that all aspects of the tool can be edited as before. For Canvas even before the tool is created in the LMS there are some options that can be configured although usually they should be left with the defaults. The only things that can be changed are if certain things in the configuration from webwork2 are enabled or not. For example, placements in the configuration from webwork2 can not be added, but can only be disabled. Also, the way the tool is presented when editing it is quite different from a manually configured tool. None of the URLs can be edited, and the things that were in the configuration from webwork can only be disabled again. Of course, it remains to be seen what D2L or Blackboard do with this. Note that there is a little more that can be added to this. Before the tool is added to the LMS, webwork2 could present a page that allows the LMS administrator to select options for the tool. For example, the current tool name will be "WeBWorK at your.webwork2.server.edu", but that could be allowed to be changed by the LMS administrator. Note that for Moodle that can be changed later anyway, but Canvas does not provide a way to change the tool name. Also, it may be desirable to allow the administrator to determine if grade passback is allowed or not. Although, for both Moodle and Canvas this can still be done in any case. Note that LTI 1.1 does support something like this, but I haven't found any documentation on it (although I haven't looked to hard).
1 parent e15c575 commit ddb0074

File tree

2 files changed

+126
-1
lines changed

2 files changed

+126
-1
lines changed

lib/WeBWorK/ContentGenerator/LTIAdvantage.pm

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package WeBWorK::ContentGenerator::LTIAdvantage;
2-
use Mojo::Base 'WeBWorK::ContentGenerator', -signatures;
2+
use Mojo::Base 'WeBWorK::ContentGenerator', -signatures, -async_await;
33

44
use Mojo::UserAgent;
55
use Mojo::JSON qw(decode_json);
66
use Crypt::JWT qw(decode_jwt encode_jwt);
77
use Math::Random::Secure qw(irand);
88
use Digest::SHA qw(sha256_hex);
9+
use Mojo::File qw(tempfile);
910

1011
use WeBWorK::Debug qw(debug);
1112
use WeBWorK::Authen::LTIAdvantage::SubmitGrade;
@@ -425,4 +426,120 @@ sub purge_expired_lti_data ($c, $ce, $db) {
425426
return;
426427
}
427428

429+
async sub registration ($c) {
430+
return $c->render(json => { error => 'invalid configuration request' }, status => 400)
431+
unless defined $c->req->param('openid_configuration') && defined $c->req->param('registration_token');
432+
433+
# If we want to allow options in the configuration such as whether grade passback is enabled or to allow the LMS
434+
# administrator to choose a tool name, then this should render a form that the LMS will be presented in an iframe
435+
# allowing the LMS administrator to select the options. When that form is submitted, then the code below should be
436+
# executed taking those options into consideration. However, at this point this is a simplistic approach that will
437+
# work in most cases.
438+
439+
$c->render_later;
440+
441+
my $configurationResult = (await Mojo::UserAgent->new->get_p($c->req->param('openid_configuration')))->result;
442+
return $c->render(json => { error => 'unabled to obtain openid configuration' }, status => 400)
443+
unless $configurationResult->is_success;
444+
my $lmsConfiguration = $configurationResult->json;
445+
446+
return $c->render(json => { error => 'invalid openid configuration received' }, status => 400)
447+
unless defined $lmsConfiguration->{registration_endpoint}
448+
&& defined $lmsConfiguration->{issuer}
449+
&& defined $lmsConfiguration->{jwks_uri}
450+
&& defined $lmsConfiguration->{token_endpoint}
451+
&& defined $lmsConfiguration->{authorization_endpoint}
452+
&& defined $lmsConfiguration->{'https://purl.imsglobal.org/spec/lti-platform-configuration'}
453+
{product_family_code};
454+
455+
# FIXME: This should also probably check that the token_endpoint_auth_method is private_key_jwt, the
456+
# id_token_signing_alg_values_supported is RS256, and that the scopes_supported is an array and contains all of the
457+
# scopes listed below. There are perhaps some other configuration values that should be checked as well. However,
458+
# most of the time these are all going to be fine.
459+
460+
my $rootURL = $c->url_for('root')->to_abs;
461+
462+
my $registrationResult = (await Mojo::UserAgent->new->post_p(
463+
$lmsConfiguration->{registration_endpoint},
464+
{
465+
Authorization => 'Bearer ' . $c->req->param('registration_token'),
466+
'Content-Type' => 'application/json'
467+
},
468+
json => {
469+
application_type => 'web',
470+
response_types => ['id_token'],
471+
grant_types => [ 'implicit', 'client_credentials' ],
472+
client_name => 'WeBWorK at ' . $rootURL->host_port,
473+
client_uri => $rootURL->to_string,
474+
initiate_login_uri => $c->url_for('ltiadvantage_login')->to_abs->to_string,
475+
redirect_uris => [ $c->url_for('ltiadvantage_launch')->to_abs->to_string ],
476+
jwks_uri => $c->url_for('ltiadvantage_keys')->to_abs->to_string,
477+
token_endpoint_auth_method => 'private_key_jwt',
478+
scope => join(' ',
479+
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem',
480+
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly',
481+
'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly',
482+
'https://purl.imsglobal.org/spec/lti-ags/scope/score'),
483+
'https://purl.imsglobal.org/spec/lti-tool-configuration' => {
484+
domain => $rootURL->host_port,
485+
target_link_uri => $rootURL->to_string,
486+
claims => [ 'iss', 'sub', 'name', 'given_name', 'family_name', 'email' ],
487+
messages => [ {
488+
type => 'LtiDeepLinkingRequest',
489+
target_link_uri => $c->url_for('ltiadvantage_content_selection')->to_abs->to_string,
490+
# Placements are specific to the LMS. The following placements are needed for Canavas, and Moodle
491+
# completely ignores this parameter. Does D2L need any? What about Blackboard?
492+
placements => [ 'assignment_selection', 'course_assignments_menu' ]
493+
} ]
494+
}
495+
}
496+
))->result;
497+
unless ($registrationResult->is_success) {
498+
$c->log->error('Invalid regististration response: ' . $registrationResult->message);
499+
return $c->render(json => { error => 'invalid registration response' }, status => 400);
500+
}
501+
return $c->render(json => { error => 'invalid registration received' }, status => 400)
502+
unless defined $registrationResult->json->{client_id};
503+
504+
my $configuration = <<~ "END_CONFIG";
505+
\$LTI{v1p3}{PlatformID} = '$lmsConfiguration->{issuer}';
506+
\$LTI{v1p3}{ClientID} = '${\($registrationResult->json->{client_id})}';
507+
\$LTI{v1p3}{DeploymentID} = '${
508+
\($registrationResult->json->{'https://purl.imsglobal.org/spec/lti-tool-configuration'}{deployment_id}
509+
// 'obtain from LMS administrator')
510+
}';
511+
\$LTI{v1p3}{PublicKeysetURL} = '$lmsConfiguration->{jwks_uri}';
512+
\$LTI{v1p3}{AccessTokenURL} = '$lmsConfiguration->{token_endpoint}';
513+
\$LTI{v1p3}{AccessTokenAUD} = '${
514+
\($lmsConfiguration->{authorization_server}
515+
// $lmsConfiguration->{token_endpoint})
516+
}';
517+
\$LTI{v1p3}{AuthReqURL} = '$lmsConfiguration->{authorization_endpoint}';
518+
END_CONFIG
519+
520+
my $registrationDir = Mojo::File->new($c->ce->{webworkDirs}{DATA})->child('LTIRegistrationRequests');
521+
if (!-d $registrationDir) {
522+
eval { $registrationDir->make_path };
523+
if ($@) {
524+
$c->log->error("Failed to create directory for saving LTI registrations: $@");
525+
return $c->render(json => { error => 'internal server error' }, status => 400);
526+
}
527+
}
528+
529+
my $registrationFile = tempfile(
530+
TEMPLATE =>
531+
$lmsConfiguration->{'https://purl.imsglobal.org/spec/lti-platform-configuration'}{product_family_code}
532+
. '-XXXX',
533+
DIR => $registrationDir,
534+
SUFFIX => '.conf',
535+
UNLINK => 0
536+
);
537+
$registrationFile->spew($configuration, 'UTF-8');
538+
539+
# This tells the LMS that registration is complete and it can close its dialog.
540+
return $c->render(data => '<script>'
541+
. q!(window.opener || window.parent).postMessage({ subject: 'org.imsglobal.lti.close' }, '*');!
542+
. '</script>');
543+
}
544+
428545
1;

lib/WeBWorK/Utils/Routes.pm

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ PLEASE FOR THE LOVE OF GOD UPDATE THIS IF YOU CHANGE THE ROUTES BELOW!!!
2323
ltiadvantage_launch /ltiadvantage/launch
2424
ltiadvantage_keys /ltiadvantage/keys
2525
ltiadvantage_content_selection /ltiadvantage/content_selection
26+
ltiadvantage_registration /ltiadvantage/registration
2627
2728
saml2_acs /saml2/acs
2829
saml2_metadata /saml2/metadata
@@ -147,6 +148,7 @@ my %routeParameters = (
147148
ltiadvantage_launch
148149
ltiadvantage_keys
149150
ltiadvantage_content_selection
151+
ltiadvantage_registration
150152
saml2_acs
151153
saml2_metadata
152154
saml2_error
@@ -217,6 +219,12 @@ my %routeParameters = (
217219
path => '/ltiadvantage/content_selection',
218220
action => 'content_selection'
219221
},
222+
ltiadvantage_registration => {
223+
title => x('LTI 1.3 Registration'),
224+
module => 'LTIAdvantage',
225+
path => '/ltiadvantage/registration',
226+
action => 'registration'
227+
},
220228

221229
# This route also ends up at the login screen on failure, and the title is not used anywhere else.
222230
saml2_acs => {

0 commit comments

Comments
 (0)