|
1 | 1 | package WeBWorK::ContentGenerator::LTIAdvantage;
|
2 |
| -use Mojo::Base 'WeBWorK::ContentGenerator', -signatures; |
| 2 | +use Mojo::Base 'WeBWorK::ContentGenerator', -signatures, -async_await; |
3 | 3 |
|
4 | 4 | use Mojo::UserAgent;
|
5 | 5 | use Mojo::JSON qw(decode_json);
|
6 | 6 | use Crypt::JWT qw(decode_jwt encode_jwt);
|
7 | 7 | use Math::Random::Secure qw(irand);
|
8 | 8 | use Digest::SHA qw(sha256_hex);
|
| 9 | +use Mojo::File qw(tempfile); |
9 | 10 |
|
10 | 11 | use WeBWorK::Debug qw(debug);
|
11 | 12 | use WeBWorK::Authen::LTIAdvantage::SubmitGrade;
|
@@ -425,4 +426,120 @@ sub purge_expired_lti_data ($c, $ce, $db) {
|
425 | 426 | return;
|
426 | 427 | }
|
427 | 428 |
|
| 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 | + |
428 | 545 | 1;
|
0 commit comments