|  | 
| 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