MDL-67301 mod_lti: dynamic registration

This commit is contained in:
Claude Vervoort 2020-02-06 10:48:11 -05:00
parent 5ecd01fb19
commit 84e90d5db4
26 changed files with 1056 additions and 63 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
define ("mod_lti/tool_configure_controller",["jquery","core/ajax","core/notification","core/templates","mod_lti/events","mod_lti/keys","mod_lti/tool_type","mod_lti/tool_proxy","core/str"],function(a,b,c,d,e,f,g,h,i){var j={EXTERNAL_REGISTRATION_CONTAINER:"#external-registration-container",EXTERNAL_REGISTRATION_PAGE_CONTAINER:"#external-registration-page-container",CARTRIDGE_REGISTRATION_CONTAINER:"#cartridge-registration-container",CARTRIDGE_REGISTRATION_FORM:"#cartridge-registration-form",ADD_TOOL_FORM:"#add-tool-form",TOOL_LIST_CONTAINER:"#tool-list-container",TOOL_CREATE_BUTTON:"#tool-create-button",REGISTRATION_CHOICE_CONTAINER:"#registration-choice-container",TOOL_URL:"#tool-url"},k=function(){return a(j.TOOL_CREATE_BUTTON)},l=function(){return a(j.TOOL_LIST_CONTAINER)},m=function(){return a(j.EXTERNAL_REGISTRATION_CONTAINER)},n=function(){return a(j.CARTRIDGE_REGISTRATION_CONTAINER)},o=function(){return a(j.REGISTRATION_CHOICE_CONTAINER)},p=function(){return a(j.TOOL_URL).val()},q=function(){m().addClass("hidden")},r=function(){n().addClass("hidden")},s=function(){o().addClass("hidden")},t=function(){r();s();m().removeClass("hidden");w(m())},u=function(a){q();s();var b=n();b.find("input").val("");b.removeClass("hidden");b.find(j.CARTRIDGE_REGISTRATION_FORM).attr("data-cartridge-url",a);w(b)},v=function(){q();r();o().removeClass("hidden");w(o())},w=function(a){var b=a.children().detach();b.appendTo(a)},x=function(){l().addClass("hidden")},y=function(){l().removeClass("hidden")},z=function(a){var b=a.error?"error":"success";c.addNotification({message:a.message,type:b})},A=function(a){a.addClass("loading")},B=function(a){a.removeClass("loading")},C=function(){var b=a.Deferred(),e=l();A(e);a.when(g.query(),h.query({orphanedonly:!0})).done(function(a,c){d.render("mod_lti/tool_list",{tools:a,proxies:c}).done(function(a,c){e.empty();e.append(a);d.runTemplateJS(c);b.resolve()}).fail(b.reject)}).fail(b.reject);b.fail(c.exception).always(function(){B(e)})},D=function(){var b=a.trim(p());if(""===b){return a.Deferred().resolve()}var d=k();A(d);var f=g.isCartridge(b);f.always(function(){B(d)});f.done(function(c){if(c.iscartridge){a(j.TOOL_URL).val("");a(document).trigger(e.START_CARTRIDGE_REGISTRATION,b)}else{a(document).trigger(e.START_EXTERNAL_REGISTRATION,{url:b})}});f.fail(function(){i.get_string("errorbadurl","mod_lti").done(function(b){a(document).trigger(e.REGISTRATION_FEEDBACK,{message:b,error:!0})}).fail(c.exception)});return f},E=function(){a(document).on(e.NEW_TOOL_TYPE,function(){C()});a(document).on(e.START_EXTERNAL_REGISTRATION,function(){t();a(j.TOOL_URL).val("");x()});a(document).on(e.STOP_EXTERNAL_REGISTRATION,function(){y();v()});a(document).on(e.START_CARTRIDGE_REGISTRATION,function(a,b){u(b)});a(document).on(e.STOP_CARTRIDGE_REGISTRATION,function(){n().find(j.CARTRIDGE_REGISTRATION_FORM).removeAttr("data-cartridge-url");v()});a(document).on(e.REGISTRATION_FEEDBACK,function(a,b){z(b)});var b=a(j.ADD_TOOL_FORM);b.submit(function(a){a.preventDefault();D()})};return{init:function init(){E();C()}}});
define ("mod_lti/tool_configure_controller",["jquery","core/ajax","core/notification","core/templates","mod_lti/events","mod_lti/keys","mod_lti/tool_type","mod_lti/tool_proxy","core/str"],function(a,b,c,d,e,f,g,h,i){var j={EXTERNAL_REGISTRATION_CONTAINER:"#external-registration-container",EXTERNAL_REGISTRATION_PAGE_CONTAINER:"#external-registration-page-container",EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER:"#external-registration-template-container",CARTRIDGE_REGISTRATION_CONTAINER:"#cartridge-registration-container",CARTRIDGE_REGISTRATION_FORM:"#cartridge-registration-form",ADD_TOOL_FORM:"#add-tool-form",TOOL_LIST_CONTAINER:"#tool-list-container",TOOL_CREATE_BUTTON:"#tool-create-button",TOOL_CREATE_LTILEGACY_BUTTON:"#tool-createltilegacy-button",REGISTRATION_CHOICE_CONTAINER:"#registration-choice-container",TOOL_URL:"#tool-url"},k=function(){return a(j.TOOL_LIST_CONTAINER)},l=function(){return a(j.EXTERNAL_REGISTRATION_CONTAINER)},m=function(){return a(j.CARTRIDGE_REGISTRATION_CONTAINER)},n=function(){return a(j.REGISTRATION_CHOICE_CONTAINER)},o=function(b){if(b.data&&"org.imsglobal.lti.close"===b.data.subject){a(j.EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER).empty();r();w();z();w();D()}},p=function(b){a(j.EXTERNAL_REGISTRATION_PAGE_CONTAINER).removeClass("hidden");var c=a(j.EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER);c.append(a("<iframe src='startltiadvregistration.php?url="+encodeURIComponent(b)+"'></iframe>"));u();window.addEventListener("message",o,!1)},q=function(){return a(j.TOOL_URL).val()},r=function(){l().addClass("hidden")},s=function(){m().addClass("hidden")},t=function(){n().addClass("hidden")},u=function(){s();t();l().removeClass("hidden");x(l())},v=function(a){r();t();var b=m();b.find("input").val("");b.removeClass("hidden");b.find(j.CARTRIDGE_REGISTRATION_FORM).attr("data-cartridge-url",a);x(b)},w=function(){r();s();n().removeClass("hidden");x(n())},x=function(a){var b=a.children().detach();b.appendTo(a)},y=function(){k().addClass("hidden")},z=function(){k().removeClass("hidden")},A=function(a){var b=a.error?"error":"success";c.addNotification({message:a.message,type:b})},B=function(a){a.addClass("loading")},C=function(a){a.removeClass("loading")},D=function(){var b=a.Deferred(),e=k();B(e);a.when(g.query(),h.query({orphanedonly:!0})).done(function(a,c){d.render("mod_lti/tool_list",{tools:a,proxies:c}).done(function(a,c){e.empty();e.append(a);d.runTemplateJS(c);b.resolve()}).fail(b.reject)}).fail(b.reject);b.fail(c.exception).always(function(){C(e)})},E=function(){var b=a.trim(q());if(b){a(j.TOOL_URL).val("");y();p(b)}},F=function(){var b=a.trim(q());if(""===b){return a.Deferred().resolve()}var d=a(j.TOOL_CREATE_LTILEGACY_BUTTON);B(d);var f=g.isCartridge(b);f.always(function(){C(d)});f.done(function(c){if(c.iscartridge){a(j.TOOL_URL).val("");a(document).trigger(e.START_CARTRIDGE_REGISTRATION,b)}else{a(document).trigger(e.START_EXTERNAL_REGISTRATION,{url:b})}});f.fail(function(){i.get_string("errorbadurl","mod_lti").done(function(b){a(document).trigger(e.REGISTRATION_FEEDBACK,{message:b,error:!0})}).fail(c.exception)});return f},G=function(){a(document).on(e.NEW_TOOL_TYPE,function(){D()});a(document).on(e.START_EXTERNAL_REGISTRATION,function(){u();a(j.TOOL_URL).val("");y()});a(document).on(e.STOP_EXTERNAL_REGISTRATION,function(){z();w()});a(document).on(e.START_CARTRIDGE_REGISTRATION,function(a,b){v(b)});a(document).on(e.STOP_CARTRIDGE_REGISTRATION,function(){m().find(j.CARTRIDGE_REGISTRATION_FORM).removeAttr("data-cartridge-url");w()});a(document).on(e.REGISTRATION_FEEDBACK,function(a,b){A(b)});var b=a(j.TOOL_CREATE_LTILEGACY_BUTTON);b.click(function(a){a.preventDefault();F()});var c=a(j.TOOL_CREATE_BUTTON);c.click(function(a){a.preventDefault();E()})};return{init:function init(){G();D()}}});
//# sourceMappingURL=tool_configure_controller.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -484,10 +484,7 @@
return toolTypeData;
}).then(function(toolTypeData) {
return templates.render('mod_lti/tool_card', toolTypeData);
}).then(function(renderResult) {
var html = renderResult[0];
var js = renderResult[1];
}).then(function(html, js) {
templates.replaceNode(element, html, js);
return;
}).catch(function() {

View File

@ -32,26 +32,17 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/e
var SELECTORS = {
EXTERNAL_REGISTRATION_CONTAINER: '#external-registration-container',
EXTERNAL_REGISTRATION_PAGE_CONTAINER: '#external-registration-page-container',
EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER: '#external-registration-template-container',
CARTRIDGE_REGISTRATION_CONTAINER: '#cartridge-registration-container',
CARTRIDGE_REGISTRATION_FORM: '#cartridge-registration-form',
ADD_TOOL_FORM: '#add-tool-form',
TOOL_LIST_CONTAINER: '#tool-list-container',
TOOL_CREATE_BUTTON: '#tool-create-button',
TOOL_CREATE_LTILEGACY_BUTTON: '#tool-createltilegacy-button',
REGISTRATION_CHOICE_CONTAINER: '#registration-choice-container',
TOOL_URL: '#tool-url'
};
/**
* Get the tool create button element.
*
* @method getToolCreateButton
* @private
* @return {Object} jQuery object
*/
var getToolCreateButton = function() {
return $(SELECTORS.TOOL_CREATE_BUTTON);
};
/**
* Get the tool list container element.
*
@ -96,6 +87,40 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/e
return $(SELECTORS.REGISTRATION_CHOICE_CONTAINER);
};
/**
* Close the LTI Advantage Registration IFrame.
*
* @private
* @param {Object} e post message event sent from the registration frame.
*/
var closeLTIAdvRegistration = function(e) {
if (e.data && 'org.imsglobal.lti.close' === e.data.subject) {
$(SELECTORS.EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER).empty();
hideExternalRegistration();
showRegistrationChoices();
showToolList();
showRegistrationChoices();
reloadToolList();
}
};
/**
* Load the external registration template and render it in the DOM and display it.
*
* @method initiateRegistration
* @private
* @param {String} url where to send the registration request
*/
var initiateRegistration = function(url) {
// Show the external registration page in an iframe.
$(SELECTORS.EXTERNAL_REGISTRATION_PAGE_CONTAINER).removeClass('hidden');
var container = $(SELECTORS.EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER);
container.append($("<iframe src='startltiadvregistration.php?url="
+ encodeURIComponent(url) + "'></iframe>"));
showExternalRegistration();
window.addEventListener("message", closeLTIAdvRegistration, false);
};
/**
* Get the tool type URL.
*
@ -287,22 +312,38 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/e
});
};
/**
* Start the LTI Advantage registration.
*
* @method addLTIAdvTool
* @private
*/
var addLTIAdvTool = function() {
var url = $.trim(getToolURL());
if (url) {
$(SELECTORS.TOOL_URL).val('');
hideToolList();
initiateRegistration(url);
}
};
/**
* Trigger appropriate registration process process for the user input
* URL. It can either be a cartridge or a registration url.
*
* @method addTool
* @method addLTILegacyTool
* @private
* @return {Promise} jQuery Deferred object
*/
var addTool = function() {
var addLTILegacyTool = function() {
var url = $.trim(getToolURL());
if (url === "") {
return $.Deferred().resolve();
}
var toolButton = getToolCreateButton();
var toolButton = $(SELECTORS.TOOL_CREATE_LTILEGACY_BUTTON);
startLoading(toolButton);
var promise = toolType.isCartridge(url);
@ -372,10 +413,16 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/e
showRegistrationFeedback(data);
});
var form = $(SELECTORS.ADD_TOOL_FORM);
form.submit(function(e) {
var addLegacyButton = $(SELECTORS.TOOL_CREATE_LTILEGACY_BUTTON);
addLegacyButton.click(function(e) {
e.preventDefault();
addTool();
addLTILegacyTool();
});
var addLTIButton = $(SELECTORS.TOOL_CREATE_BUTTON);
addLTIButton.click(function(e) {
e.preventDefault();
addLTIAdvTool();
});
};

View File

@ -21,28 +21,13 @@
* @copyright 2019 Stephen Vickers
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_lti\local\ltiopenid\jwks_helper;
define('NO_DEBUG_DISPLAY', true);
define('NO_MOODLE_COOKIES', true);
require_once(__DIR__ . '/../../config.php');
$jwks = array('keys' => array());
$privatekey = get_config('mod_lti', 'privatekey');
$res = openssl_pkey_get_private($privatekey);
$details = openssl_pkey_get_details($res);
$jwk = array();
$jwk['kty'] = 'RSA';
$jwk['alg'] = 'RS256';
$jwk['kid'] = get_config('mod_lti', 'kid');
$jwk['e'] = rtrim(strtr(base64_encode($details['rsa']['e']), '+/', '-_'), '=');
$jwk['n'] = rtrim(strtr(base64_encode($details['rsa']['n']), '+/', '-_'), '=');
$jwk['use'] = 'sig';
$jwks['keys'][] = $jwk;
@header('Content-Type: application/json; charset=utf-8');
echo json_encode($jwks, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
echo json_encode(jwks_helper::get_jwks(), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);

View File

@ -0,0 +1,72 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This files exposes functions for LTI 1.3 Key Management.
*
* @package mod_lti
* @copyright 2020 Claude Vervoort (Cengage)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_lti\local\ltiopenid;
/**
* This class exposes functions for LTI 1.3 Key Management.
*
* @package mod_lti
* @copyright 2020 Claude Vervoort (Cengage)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class jwks_helper {
/**
* Returns the private key to use to sign outgoing JWT.
*
* @return array keys are kid and key in PEM format.
*/
public static function get_private_key() {
$privatekey = get_config('mod_lti', 'privatekey');
$kid = get_config('mod_lti', 'kid');
return [
"key" => $privatekey,
"kid" => $kid
];
}
/**
* Returns the JWK Key Set for this site.
* @return array keyset exposting the site public key.
*/
public static function get_jwks() {
$jwks = array('keys' => array());
$privatekey = self::get_private_key();
$res = openssl_pkey_get_private($privatekey['key']);
$details = openssl_pkey_get_details($res);
$jwk = array();
$jwk['kty'] = 'RSA';
$jwk['alg'] = 'RS256';
$jwk['kid'] = $privatekey['kid'];
$jwk['e'] = rtrim(strtr(base64_encode($details['rsa']['e']), '+/', '-_'), '=');
$jwk['n'] = rtrim(strtr(base64_encode($details['rsa']['n']), '+/', '-_'), '=');
$jwk['use'] = 'sig';
$jwks['keys'][] = $jwk;
return $jwks;
}
}

View File

@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This library exposes functions for LTI Dynamic Registration.
*
* @package mod_lti
* @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_lti\local\ltiopenid;
/**
* Exception when transforming the registration to LTI config.
*
* Code is the HTTP Error code.
*/
class registration_exception extends \Exception {
}

View File

@ -0,0 +1,345 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A Helper for LTI Dynamic Registration.
*
* @package mod_lti
* @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_lti\local\ltiopenid;
defined('MOODLE_INTERNAL') || die;
require_once($CFG->dirroot . '/mod/lti/locallib.php');
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
use stdClass;
/**
* This class exposes functions for LTI Dynamic Registration.
*
* @package mod_lti
* @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class registration_helper {
/** score scope */
const SCOPE_SCORE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score';
/** result scope */
const SCOPE_RESULT = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly';
/** lineitem read-only scope */
const SCOPE_LINEITEM_RO = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly';
/** lineitem full access scope */
const SCOPE_LINEITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem';
/** Names and Roles (membership) scope */
const SCOPE_NRPS = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly';
/** Tool Settings scope */
const SCOPE_TOOL_SETTING = 'https://purl.imsglobal.org/spec/lti-ts/scope/toolsetting';
/**
* Function used to validate parameters.
*
* This function is needed because the payload contains nested
* objects, and optional_param() does not support arrays of arrays.
*
* @param array $payload that may contain the parameter key
* @param string $key the key of the value to be looked for in the payload
* @param bool $required if required, not finding a value will raise a registration_exception
*
* @return mixed
*/
private static function get_parameter(array $payload, string $key, bool $required) {
if (!isset($payload[$key]) || empty($payload[$key])) {
if ($required) {
throw new registration_exception('missing required attribute '.$key, 400);
}
return null;
}
$parameter = $payload[$key];
// Cleans parameters to avoid XSS and other issues.
if (is_array($parameter)) {
return clean_param_array($parameter, PARAM_TEXT, true);
}
return clean_param($parameter, PARAM_TEXT);
}
/**
* Transforms an LTI 1.3 Registration to a Moodle LTI Config.
*
* @param array $registrationpayload the registration data received from the tool.
* @param string $clientid the clientid to be issued for that tool.
*
* @return object the Moodle LTI config.
*/
public static function registration_to_config(array $registrationpayload, string $clientid): object {
$responsetypes = self::get_parameter($registrationpayload, 'response_types', true);
$initiateloginuri = self::get_parameter($registrationpayload, 'initiate_login_uri', true);
$redirecturis = self::get_parameter($registrationpayload, 'redirect_uris', true);
$clientname = self::get_parameter($registrationpayload, 'client_name', true);
$jwksuri = self::get_parameter($registrationpayload, 'jwks_uri', true);
$tokenendpointauthmethod = self::get_parameter($registrationpayload, 'token_endpoint_auth_method', true);
$applicationtype = self::get_parameter($registrationpayload, 'application_type', false);
$logouri = self::get_parameter($registrationpayload, 'logo_uri', false);
$ltitoolconfiguration = self::get_parameter($registrationpayload,
'https://purl.imsglobal.org/spec/lti-tool-configuration', true);
$domain = self::get_parameter($ltitoolconfiguration, 'domain', true);
$targetlinkuri = self::get_parameter($ltitoolconfiguration, 'target_link_uri', true);
$customparameters = self::get_parameter($ltitoolconfiguration, 'custom_parameters', false);
$scopes = explode(" ", self::get_parameter($registrationpayload, 'scope', false) ?? '');
$claims = self::get_parameter($ltitoolconfiguration, 'claims', false);
$messages = $ltitoolconfiguration['messages'] ?? [];
$description = self::get_parameter($ltitoolconfiguration, 'description', false);
// Validate response type.
// According to specification, for this scenario, id_token must be explicitly set.
if (!in_array('id_token', $responsetypes)) {
throw new registration_exception('invalid_response_types', 400);
}
// According to specification, this parameter needs to be an array.
if (!is_array($redirecturis)) {
throw new registration_exception('invalid_redirect_uris', 400);
}
// According to specification, for this scenario private_key_jwt must be explicitly set.
if ($tokenendpointauthmethod !== 'private_key_jwt') {
throw new registration_exception('invalid_token_endpoint_auth_method', 400);
}
if (!empty($applicationtype) && $applicationtype !== 'web') {
throw new registration_exception('invalid_application_type', 400);
}
$config = new stdClass();
$config->lti_clientid = $clientid;
$config->lti_toolurl = $targetlinkuri;
$config->lti_tooldomain = $domain;
$config->lti_typename = $clientname;
$config->lti_description = $description;
$config->lti_ltiversion = LTI_VERSION_1P3;
$config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEID;
$config->lti_icon = $logouri;
$config->lti_coursevisible = LTI_COURSEVISIBLE_PRECONFIGURED;
$config->lti_contentitem = 0;
// Sets Content Item.
if (!empty($messages)) {
$messagesresponse = [];
foreach ($messages as $value) {
if ($value['type'] === 'LtiDeepLinkingRequest') {
$config->lti_contentitem = 1;
$config->lti_toolurl_ContentItemSelectionRequest = $value['target_link_uri'] ?? '';
array_push($messagesresponse, $value);
}
}
}
$config->lti_keytype = 'JWK_KEYSET';
$config->lti_publickeyset = $jwksuri;
$config->lti_initiatelogin = $initiateloginuri;
$config->lti_redirectionuris = implode(PHP_EOL, $redirecturis);
$config->lti_customparameters = '';
// Sets custom parameters.
if (isset($customparameters)) {
$paramssarray = [];
foreach ($customparameters as $key => $value) {
array_push($paramssarray, $key . '=' . $value);
}
$config->lti_customparameters = implode(PHP_EOL, $paramssarray);
}
// Sets launch container.
$config->lti_launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
// Sets Service info based on scopes.
$config->lti_acceptgrades = LTI_SETTING_NEVER;
$config->ltiservice_gradesynchronization = 0;
$config->ltiservice_memberships = 0;
$config->ltiservice_toolsettings = 0;
if (isset($scopes)) {
// Sets Assignment and Grade Services info.
if (in_array(self::SCOPE_SCORE, $scopes)) {
$config->lti_acceptgrades = LTI_SETTING_DELEGATE;
$config->ltiservice_gradesynchronization = 1;
}
if (in_array(self::SCOPE_RESULT, $scopes)) {
$config->lti_acceptgrades = LTI_SETTING_DELEGATE;
$config->ltiservice_gradesynchronization = 1;
}
if (in_array(self::SCOPE_LINEITEM_RO, $scopes)) {
$config->lti_acceptgrades = LTI_SETTING_DELEGATE;
$config->ltiservice_gradesynchronization = 1;
}
if (in_array(self::SCOPE_LINEITEM, $scopes)) {
$config->lti_acceptgrades = LTI_SETTING_DELEGATE;
$config->ltiservice_gradesynchronization = 2;
}
// Sets Names and Role Provisioning info.
if (in_array(self::SCOPE_NRPS, $scopes)) {
$config->ltiservice_memberships = 1;
}
// Sets Tool Settings info.
if (in_array(self::SCOPE_TOOL_SETTING, $scopes)) {
$config->ltiservice_toolsettings = 1;
}
}
// Sets privacy settings.
$config->lti_sendname = LTI_SETTING_NEVER;
$config->lti_sendemailaddr = LTI_SETTING_NEVER;
if (isset($claims)) {
// Sets name privacy settings.
if (in_array('name', $claims)) {
$config->lti_sendname = LTI_SETTING_ALWAYS;
}
if (in_array('given_name', $claims)) {
$config->lti_sendname = LTI_SETTING_ALWAYS;
}
if (in_array('family_name', $claims)) {
$config->lti_sendname = LTI_SETTING_ALWAYS;
}
// Sets email privacy settings.
if (in_array('email', $claims)) {
$config->lti_sendemailaddr = LTI_SETTING_ALWAYS;
}
}
return $config;
}
/**
* Transforms a moodle LTI 1.3 Config to an OAuth/LTI Client Registration.
*
* @param object $config Moodle LTI Config.
* @param int $typeid which is the LTI deployment id.
*
* @return array the Client Registration as an associative array.
*/
public static function config_to_registration(object $config, int $typeid): array {
$registrationresponse = [];
$registrationresponse['client_id'] = $config->lti_clientid;
$registrationresponse['token_endpoint_auth_method'] = ['private_key_jwt'];
$registrationresponse['response_types'] = ['id_token'];
$registrationresponse['jwks_uri'] = $config->lti_publickeyset;
$registrationresponse['initiate_login_uri'] = $config->lti_initiatelogin;
$registrationresponse['grant_types'] = ['client_credentials', 'implicit'];
$registrationresponse['redirect_uris'] = explode(PHP_EOL, $config->lti_redirectionuris);
$registrationresponse['application_type'] = ['web'];
$registrationresponse['token_endpoint_auth_method'] = 'private_key_jwt';
$registrationresponse['client_name'] = $config->lti_typename;
$registrationresponse['logo_uri'] = $config->lti_icon ?? '';
$lticonfigurationresponse = [];
$lticonfigurationresponse['deployment_id'] = strval($typeid);
$lticonfigurationresponse['target_link_uri'] = $config->lti_toolurl;
$lticonfigurationresponse['domain'] = $config->lti_tooldomain ?? '';
$lticonfigurationresponse['description'] = $config->lti_description ?? '';
if ($config->lti_contentitem == 1) {
$contentitemmessage = [];
$contentitemmessage['type'] = 'LtiDeepLinkingRequest';
if (isset($config->lti_toolurl_ContentItemSelectionRequest)) {
$contentitemmessage['target_link_uri'] = $config->lti_toolurl_ContentItemSelectionRequest;
}
$lticonfigurationresponse['messages'] = [$contentitemmessage];
}
if (isset($config->lti_customparameters) && !empty($config->lti_customparameters)) {
$params = [];
foreach (explode(PHP_EOL, $config->lti_customparameters) as $param) {
$split = explode('=', $param);
$params[$split[0]] = $split[1];
}
$lticonfigurationresponse['custom_parameters'] = $params;
}
$scopesresponse = [];
if ($config->ltiservice_gradesynchronization > 0) {
$scopesresponse[] = self::SCOPE_SCORE;
$scopesresponse[] = self::SCOPE_RESULT;
$scopesresponse[] = self::SCOPE_LINEITEM_RO;
}
if ($config->ltiservice_gradesynchronization == 2) {
$scopesresponse[] = self::SCOPE_LINEITEM;
}
if ($config->ltiservice_memberships == 1) {
$scopesresponse[] = self::SCOPE_NRPS;
}
if ($config->ltiservice_toolsettings == 1) {
$scopesresponse[] = self::SCOPE_TOOL_SETTING;
}
$registrationresponse['scope'] = implode(' ', $scopesresponse);
$claimsresponse = ['sub', 'iss'];
if ($config->lti_sendname = LTI_SETTING_ALWAYS) {
$claimsresponse[] = 'name';
$claimsresponse[] = 'family_name';
$claimsresponse[] = 'middle_name';
}
if ($config->lti_sendemailaddr = LTI_SETTING_ALWAYS) {
$claimsresponse[] = 'email';
}
$lticonfigurationresponse['claims'] = $claimsresponse;
$registrationresponse['https://purl.imsglobal.org/spec/lti-tool-configuration'] = $lticonfigurationresponse;
return $registrationresponse;
}
/**
* Validates the registration token is properly signed and not used yet.
* Return the client id to use for this registration.
*
* @param string $registrationtokenjwt registration token
*
* @return string client id for the registration
*/
public static function validate_registration_token(string $registrationtokenjwt): string {
global $DB;
$keys = JWK::parseKeySet(jwks_helper::get_jwks());
$registrationtoken = JWT::decode($registrationtokenjwt, $keys, ['RS256']);
// Get clientid from registrationtoken.
$clientid = $registrationtoken->sub;
// Checks if clientid is already registered.
if (!empty($DB->get_record('lti_types', array('clientid' => $clientid)))) {
throw new registration_exception("token_already_used", 401);
}
return $clientid;
}
/**
* Initializes an array with the scopes for services supported by the LTI module
*
* @return array List of scopes
*/
public static function lti_get_service_scopes() {
$services = lti_get_services();
$scopes = array();
foreach ($services as $service) {
$servicescopes = $service->get_scopes();
if (!empty($servicescopes)) {
$scopes = array_merge($scopes, $servicescopes);
}
}
return $scopes;
}
}

View File

@ -207,7 +207,7 @@ abstract class service_base {
abstract public function get_resources();
/**
* Get the scope(s) permitted for this service.
* Get the scope(s) permitted for this service in the context of a particular tool type.
*
* A null value indicates that no scopes are required to access the service.
*
@ -217,6 +217,17 @@ abstract class service_base {
return null;
}
/**
* Get the scope(s) permitted for this service.
*
* A null value indicates that no scopes are required to access the service.
*
* @return array|null
*/
public function get_scopes() {
return null;
}
/**
* Returns the configuration options for this service.
*

View File

@ -66,6 +66,8 @@ $string['activate'] = 'Activate';
$string['activatetoadddescription'] = 'You will need to activate this tool before you can add a description.';
$string['active'] = 'Active';
$string['activity'] = 'Activity';
$string['add_ltiadv'] = 'Add LTI Advantage';
$string['add_ltilegacy'] = 'Add Legacy LTI';
$string['addnewapp'] = 'Enable external application';
$string['addserver'] = 'Add new trusted server';
$string['addtype'] = 'Add preconfigured tool';

View File

@ -54,6 +54,7 @@ defined('MOODLE_INTERNAL') || die;
use moodle\mod\lti as lti;
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
use mod_lti\local\ltiopenid\jwks_helper;
global $CFG;
require_once($CFG->dirroot.'/mod/lti/OAuth.php');
@ -2720,7 +2721,11 @@ function lti_get_type_type_config($id) {
function lti_prepare_type_for_save($type, $config) {
if (isset($config->lti_toolurl)) {
$type->baseurl = $config->lti_toolurl;
$type->tooldomain = lti_get_domain_from_url($config->lti_toolurl);
if (isset($config->lti_tooldomain)) {
$type->tooldomain = $config->lti_tooldomain;
} else {
$type->tooldomain = lti_get_domain_from_url($config->lti_toolurl);
}
}
if (isset($config->lti_description)) {
$type->description = $config->lti_description;
@ -3273,9 +3278,8 @@ function lti_sign_jwt($parms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce
}
}
$privatekey = get_config('mod_lti', 'privatekey');
$kid = get_config('mod_lti', 'kid');
$jwt = JWT::encode($payload, $privatekey, 'RS256', $kid);
$privatekey = jwks_helper::get_private_key();
$jwt = JWT::encode($payload, $privatekey['key'], 'RS256', $privatekey['kid']);
$newparms = array();
$newparms['id_token'] = $jwt;
@ -3820,6 +3824,7 @@ function lti_get_service_by_resource_id($services, $resourceid) {
/**
* Initializes an array with the scopes for services supported by the LTI module
* and authorized for this particular tool instance.
*
* @param object $type LTI tool type
* @param array $typeconfig LTI tool type configuration
@ -3840,7 +3845,6 @@ function lti_get_permitted_service_scopes($type, $typeconfig) {
}
return $scopes;
}
/**
@ -4455,3 +4459,4 @@ function lti_new_access_token($typeid, $scopes) {
return $newtoken;
}

View File

@ -0,0 +1,61 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file returns the OpenId/LTI Configuration for this site.
*
* It is part of the LTI Tool Dynamic Registration, and used by
* tools to get the site configuration and registration end-point.
*
* @package mod_lti
* @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_lti\local\ltiopenid\registration_helper;
define('NO_DEBUG_DISPLAY', true);
define('NO_MOODLE_COOKIES', true);
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/lti/locallib.php');
require_once($CFG->libdir.'/weblib.php');
$scopes = registration_helper::lti_get_service_scopes();
$scopes[] = 'openid';
$conf = [
'issuer' => $CFG->wwwroot,
'token_endpoint' => (new moodle_url('/mod/lti/token.php'))->out(false),
'token_endpoint_auth_methods_supported' => ['private_key_jwt'],
'token_endpoint_auth_signing_alg_values_supported' => ['RS256'],
'jwks_uri' => (new moodle_url('/mod/lti/certs.php'))->out(false),
'registration_endpoint' => (new moodle_url('/mod/lti/openid-registration.php'))->out(false),
'scopes_supported' => $scopes,
'response_types_supported' => ['id_token'],
'subject_types_supported' => ['public', 'pairwise'],
'id_token_signing_alg_values_supported' => ['RS256'],
'claims_supported' => ['sub', 'iss', 'name', 'given_name', 'family_name', 'email'],
'https://purl.imsglobal.org/spec/lti-platform-configuration ' => [
'product_family_code' => 'moodle',
'version' => $CFG->release,
'messages_supported' => ['LtiResourceLink', 'LtiDeepLinkingRequest'],
'placements' => ['AddContentMenu'],
'variables' => array_keys(lti_get_capabilities())
]
];
@header('Content-Type: application/json; charset=utf-8');
echo json_encode($conf, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);

View File

@ -0,0 +1,62 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file receives a registration request along with the registration token and returns a client_id.
*
* @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill)
* @package mod_lti
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('NO_DEBUG_DISPLAY', true);
define('NO_MOODLE_COOKIES', true);
use mod_lti\local\ltiopenid\registration_helper;
use mod_lti\local\ltiopenid\registration_exception;
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/lti/locallib.php');
$code = 200;
$message = '';
// Retrieve registration token from Bearer Authorization header.
$authheader = moodle\mod\lti\OAuthUtil::get_headers() ['Authorization'] ?? '';
if (!($authheader && substr($authheader, 0, 7) == 'Bearer ')) {
$message = 'missing_registration_token';
$code = 401;
} else {
$registrationpayload = json_decode(file_get_contents('php://input'), true);
// Registers tool.
$type = new stdClass();
$type->state = LTI_TOOL_STATE_PENDING;
try {
$clientid = registration_helper::validate_registration_token(trim(substr($authheader, 7)));
$config = registration_helper::registration_to_config($registrationpayload, $clientid);
$typeid = lti_add_type($type, clone $config);
$message = json_encode(registration_helper::config_to_registration($config, $typeid));
header('Content-Type: application/json; charset=utf-8');
} catch (registration_exception $e) {
$code = $e->getCode();
$message = $e->getMessage();
}
}
$response = new \mod_lti\local\ltiservice\response();
// Set code.
$response->set_code($code);
// Set body.
$response->set_body($message);
$response->send();

View File

@ -81,4 +81,13 @@ class basicoutcomes extends \mod_lti\local\ltiservice\service_base {
}
/**
* Get the scope(s) permitted for the tool relevant to this service.
*
* @return array
*/
public function get_scopes() {
return [self::SCOPE_BASIC_OUTCOMES];
}
}

View File

@ -112,6 +112,16 @@ class gradebookservices extends service_base {
}
/**
* Get the scopes defined by this service.
*
* @return array
*/
public function get_scopes() {
return [self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ, self::SCOPE_GRADEBOOKSERVICES_RESULT_READ,
self::SCOPE_GRADEBOOKSERVICES_SCORE, self::SCOPE_GRADEBOOKSERVICES_LINEITEM];
}
/**
* Adds form elements for gradebook sync add/edit page.
*

View File

@ -102,6 +102,15 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
}
/**
* Get the scope(s) defined by this service.
*
* @return array
*/
public function get_scopes() {
return [self::SCOPE_MEMBERSHIPS_READ];
}
/**
* Get the JSON for members.
*

View File

@ -88,6 +88,15 @@ class toolsettings extends \mod_lti\local\ltiservice\service_base {
}
/**
* Get the scope(s) defined this service.
*
* @return array
*/
public function get_scopes() {
return [self::SCOPE_TOOL_SETTINGS];
}
/**
* Get the distinct settings from each level by removing any duplicates from higher levels.
*

View File

@ -0,0 +1,51 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Redirect the user to registration with token and openid config url as query params.
*
* @package mod_lti
* @copyright 2020 Cengage
* @author Claude Vervoort
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use Firebase\JWT\JWT;
use mod_lti\local\ltiopenid\jwks_helper;
require_once(__DIR__ . '/../../config.php');
require_once($CFG->libdir.'/weblib.php');
require_login();
$context = context_system::instance();
require_capability('moodle/site:config', $context);
$starturl = required_param('url', PARAM_URL);
$now = time();
$token = [
"sub" => random_string(15),
"scope" => "reg",
"iat" => $now,
"exp" => $now + HOURSECS
];
$privatekey = jwks_helper::get_private_key();
$regtoken = JWT::encode($token, $privatekey['key'], 'RS256', $privatekey['kid']);
$confurl = new moodle_url('/mod/lti/openid-configuration.php');
$url = new moodle_url($starturl);
$url->param('openid_configuration', $confurl->out(false));
$url->param('registration_token', $regtoken);
header("Location: ".$url->out(false));

View File

@ -75,15 +75,15 @@
<div class="controls">
<button id="cartridge-registration-submit" type="submit" class="btn btn-success">
<span class="btn-text">{{#str}} savechanges {{/str}}</span>
<div class="btn-loader">
<span class="btn-loader">
{{> mod_lti/loader }}
</div>
</span>
</button>
<button id="cartridge-registration-cancel" type="button" class="btn">
<span class="btn-text">{{#str}} cancel {{/str}}</span>
<div class="btn-loader">
<span class="btn-loader">
{{> mod_lti/loader }}
</div>
</span>
</button>
</div>
</div>

View File

@ -29,13 +29,17 @@
Context variables required for this template:
*
Example context (json):
{
}
}}
<div id="external-registration-page-container">
<button id="cancel-external-registration" class="btn btn-danger">
<span class="btn-text">{{#str}} cancel {{/str}}</span>
<div class="btn-loader">
<span class="btn-loader">
{{> mod_lti/loader }}
</div>
</span>
</button>
<div id="external-registration-template-container"></div>
</div>

View File

@ -34,6 +34,6 @@
}
}}
<div class="loader">
<span class="loader">
{{#pix}} i/loading, core, {{#str}} loadinghelp, moodle {{/str}} {{/pix}}
</div>
</span>

View File

@ -29,6 +29,13 @@
Context variables required for this template:
*
Example context (json):
{
"configuremanualurl":"https://some.tool.example/mod/lti/typessettings.php?sesskey=OKl37bHflL&amp;returnto=toolconfigure",
"managetoolsurl":"https://some.tool.example/admin/settings.php?section=modsettinglti",
"managetoolproxiesurl":"https://some.tool.example/mod/lti/toolproxies.php"
}
}}
<h2>{{#str}} manage_external_tools, mod_lti {{/str}}</h2>
<div id="main-content-container">
@ -46,10 +53,16 @@
placeholder="{{#str}} toolurlplaceholder, mod_lti {{/str}}"
required>
<button id="tool-create-button" type="submit" class="btn btn-success">
<span class="btn-text">{{#str}} add {{/str}}</span>
<div class="btn-loader">
<span class="btn-text">{{#str}} add_ltiadv, mod_lti {{/str}}</span>
<span class="btn-loader">
{{> mod_lti/loader }}
</div>
</span>
</button>
<button id="tool-createltilegacy-button" type="button" class="btn btn-warning">
<span class="btn-text">{{#str}} add_ltilegacy, mod_lti {{/str}}</span>
<span class="btn-loader">
{{> mod_lti/loader }}
</span>
</button>
</div>
</form>

View File

@ -22,7 +22,7 @@ Feature: Configure tool types
@javascript
Scenario: Add a tool type from a cartridge URL
When I set the field "url" to local url "/mod/lti/tests/fixtures/ims_cartridge_basic_lti_link.xml"
And I press "Add"
And I press "Add Legacy LTI"
Then I should see "Enter your consumer key and shared secret"
And I press "Save changes"
And I should see "Example tool"
@ -30,7 +30,7 @@ Feature: Configure tool types
@javascript
Scenario: Try to add a non-existant cartridge
When I set the field "url" to local url "/mod/lti/tests/fixtures/nonexistant.xml"
And I press "Add"
And I press "Add Legacy LTI"
Then I should see "Enter your consumer key and shared secret"
And I press "Save changes"
And I should see "Failed to create new tool. Please check the URL and try again."
@ -38,6 +38,6 @@ Feature: Configure tool types
@javascript
Scenario: Attempt to add a tool type from a configuration URL, then cancel
When I set the field "url" to local url "/mod/lti/tests/fixtures/tool_provider.php"
And I press "Add"
And I press "Add Legacy LTI"
Then I should see "Cancel"
And I press "cancel-external-registration"

View File

@ -0,0 +1,269 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
//
// This file is part of BasicLTI4Moodle
//
// BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability)
// consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web
// based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI
// specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS
// are already supporting or going to support BasicLTI. This project Implements the consumer
// for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas.
// BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem
// at the GESSI research group at UPC.
// SimpleLTI consumer for Moodle is an implementation of the early specification of LTI
// by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a
// Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier.
//
// BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis
// of the Universitat Politecnica de Catalunya http://www.upc.edu
// Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu.
/**
* This file contains unit tests for lti/openidregistrationlib.php
*
* @package mod_lti
* @copyright 2020 Claude Vervoort, Cengage
* @author Claude Vervoort
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_lti\local\ltiopenid\registration_exception;
use mod_lti\local\ltiopenid\registration_helper;
/**
* OpenId LTI Registration library tests
*/
class mod_lti_openidregistrationlib_testcase extends advanced_testcase {
/**
* @var string A has-it-all client registration.
*/
private $registrationfulljson = <<<EOD
{
"application_type": "web",
"response_types": ["id_token"],
"grant_types": ["implict", "client_credentials"],
"initiate_login_uri": "https://client.example.org/lti/init",
"redirect_uris":
["https://client.example.org/callback",
"https://client.example.org/callback2"],
"client_name": "Virtual Garden",
"client_name#ja": "バーチャルガーデン",
"jwks_uri": "https://client.example.org/.well-known/jwks.json",
"logo_uri": "https://client.example.org/logo.png",
"policy_uri": "https://client.example.org/privacy",
"policy_uri#ja": "https://client.example.org/privacy?lang=ja",
"tos_uri": "https://client.example.org/tos",
"tos_uri#ja": "https://client.example.org/tos?lang=ja",
"token_endpoint_auth_method": "private_key_jwt",
"contacts": ["ve7jtb@example.org", "mary@example.org"],
"scope": "https://purl.imsglobal.org/spec/lti-ags/scope/score https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
"https://purl.imsglobal.org/spec/lti-tool-configuration": {
"domain": "client.example.org",
"description": "Learn Botany by tending to your little (virtual) garden.",
"description#ja": "小さな(仮想)庭に行くことで植物学を学びましょう。",
"target_link_uri": "https://client.example.org/lti",
"custom_parameters": {
"context_history": "\$Context.id.history"
},
"claims": ["iss", "sub", "name", "given_name", "family_name", "email"],
"messages": [
{
"type": "LtiDeepLinkingRequest",
"target_link_uri": "https://client.example.org/lti/dl",
"label": "Add a virtual garden",
"label#ja": "バーチャルガーデンを追加する"
}
]
}
}
EOD;
/**
* @var string A minimalist client registration.
*/
private $registrationminimaljson = <<<EOD
{
"application_type": "web",
"response_types": ["id_token"],
"grant_types": ["implict", "client_credentials"],
"initiate_login_uri": "https://client.example.org/lti/init",
"redirect_uris":
["https://client.example.org/callback"],
"client_name": "Virtual Garden",
"jwks_uri": "https://client.example.org/.well-known/jwks.json",
"token_endpoint_auth_method": "private_key_jwt",
"https://purl.imsglobal.org/spec/lti-tool-configuration": {
"domain": "client.example.org",
"target_link_uri": "https://client.example.org/lti"
}
}
EOD;
/**
* @var string A minimalist with deep linking client registration.
*/
private $registrationminimaldljson = <<<EOD
{
"application_type": "web",
"response_types": ["id_token"],
"grant_types": ["implict", "client_credentials"],
"initiate_login_uri": "https://client.example.org/lti/init",
"redirect_uris":
["https://client.example.org/callback"],
"client_name": "Virtual Garden",
"jwks_uri": "https://client.example.org/.well-known/jwks.json",
"token_endpoint_auth_method": "private_key_jwt",
"https://purl.imsglobal.org/spec/lti-tool-configuration": {
"domain": "client.example.org",
"target_link_uri": "https://client.example.org/lti",
"messages": [
{
"type": "LtiDeepLinkingRequest"
}
]
}
}
EOD;
/**
* Test the mapping from Registration JSON to LTI Config for a has-it-all tool registration.
*/
public function test_to_config_full() {
$registration = json_decode($this->registrationfulljson, true);
$registration['scope'] .= ' https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly';
$config = registration_helper::registration_to_config($registration, 'TheClientId');
$this->assertEquals('JWK_KEYSET', $config->lti_keytype);
$this->assertEquals(LTI_VERSION_1P3, $config->lti_ltiversion);
$this->assertEquals('TheClientId', $config->lti_clientid);
$this->assertEquals('Virtual Garden', $config->lti_typename);
$this->assertEquals('Learn Botany by tending to your little (virtual) garden.', $config->lti_description);
$this->assertEquals('https://client.example.org/lti/init', $config->lti_initiatelogin);
$this->assertEquals(implode(PHP_EOL, ["https://client.example.org/callback",
"https://client.example.org/callback2"]), $config->lti_redirectionuris);
$this->assertEquals("context_history=\$Context.id.history", $config->lti_customparameters);
$this->assertEquals("https://client.example.org/.well-known/jwks.json", $config->lti_publickeyset);
$this->assertEquals("https://client.example.org/logo.png", $config->lti_icon);
$this->assertEquals(2, $config->ltiservice_gradesynchronization);
$this->assertEquals(LTI_SETTING_DELEGATE, $config->lti_acceptgrades);
$this->assertEquals(1, $config->ltiservice_memberships);
$this->assertEquals(0, $config->ltiservice_toolsettings);
$this->assertEquals(LTI_SETTING_ALWAYS, $config->lti_sendname);
$this->assertEquals(LTI_SETTING_ALWAYS, $config->lti_sendemailaddr);
$this->assertEquals(1, $config->lti_contentitem);
$this->assertEquals('https://client.example.org/lti/dl', $config->lti_toolurl_ContentItemSelectionRequest);
}
/**
* Test the mapping from Registration JSON to LTI Config for a minimal tool registration.
*/
public function test_to_config_minimal() {
$registration = json_decode($this->registrationminimaljson, true);
$config = registration_helper::registration_to_config($registration, 'TheClientId');
$this->assertEquals('JWK_KEYSET', $config->lti_keytype);
$this->assertEquals(LTI_VERSION_1P3, $config->lti_ltiversion);
$this->assertEquals('TheClientId', $config->lti_clientid);
$this->assertEquals('Virtual Garden', $config->lti_typename);
$this->assertEmpty($config->lti_description);
$this->assertEquals('https://client.example.org/lti/init', $config->lti_initiatelogin);
$this->assertEquals('https://client.example.org/callback', $config->lti_redirectionuris);
$this->assertEmpty($config->lti_customparameters);
$this->assertEquals("https://client.example.org/.well-known/jwks.json", $config->lti_publickeyset);
$this->assertEmpty($config->lti_icon);
$this->assertEquals(0, $config->ltiservice_gradesynchronization);
$this->assertEquals(LTI_SETTING_NEVER, $config->lti_acceptgrades);
$this->assertEquals(0, $config->ltiservice_memberships);
$this->assertEquals(LTI_SETTING_NEVER, $config->lti_sendname);
$this->assertEquals(LTI_SETTING_NEVER, $config->lti_sendemailaddr);
$this->assertEquals(0, $config->lti_contentitem);
}
/**
* Test the mapping from Registration JSON to LTI Config for a minimal tool with
* deep linking support registration.
*/
public function test_to_config_minimal_with_deeplinking() {
$registration = json_decode($this->registrationminimaldljson, true);
$config = registration_helper::registration_to_config($registration, 'TheClientId');
$this->assertEquals(1, $config->lti_contentitem);
$this->assertEmpty($config->lti_toolurl_ContentItemSelectionRequest);
}
/**
* Validation Test: initiation login.
*/
public function test_validation_initlogin() {
$registration = json_decode($this->registrationfulljson, true);
$this->expectException(registration_exception::class);
$this->expectExceptionCode(400);
unset($registration['initiate_login_uri']);
registration_helper::registration_to_config($registration, 'TheClientId');
}
/**
* Validation Test: redirect uris.
*/
public function test_validation_redirecturis() {
$registration = json_decode($this->registrationfulljson, true);
$this->expectException(registration_exception::class);
$this->expectExceptionCode(400);
unset($registration['redirect_uris']);
registration_helper::registration_to_config($registration, 'TheClientId');
}
/**
* Validation Test: jwks uri empty.
*/
public function test_validation_jwks() {
$registration = json_decode($this->registrationfulljson, true);
$this->expectException(registration_exception::class);
$this->expectExceptionCode(400);
$registration['jwks_uri'] = '';
registration_helper::registration_to_config($registration, 'TheClientId');
}
/**
* Test the transformation from lti config to OpenId LTI Client Registration response.
*/
public function test_config_to_registration() {
$orig = json_decode($this->registrationfulljson, true);
$orig['scope'] .= ' https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly';
$reg = registration_helper::config_to_registration(registration_helper::registration_to_config($orig, 'clid'), 12);
$this->assertEquals('clid', $reg['client_id']);
$this->assertEquals($orig['response_types'], $reg['response_types']);
$this->assertEquals($orig['initiate_login_uri'], $reg['initiate_login_uri']);
$this->assertEquals($orig['redirect_uris'], $reg['redirect_uris']);
$this->assertEquals($orig['jwks_uri'], $reg['jwks_uri']);
$this->assertEquals($orig['logo_uri'], $reg['logo_uri']);
$this->assertEquals('https://purl.imsglobal.org/spec/lti-ags/scope/score '.
'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly '.
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly '.
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem '.
'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly', $reg['scope']);
$ltiorig = $orig['https://purl.imsglobal.org/spec/lti-tool-configuration'];
$lti = $reg['https://purl.imsglobal.org/spec/lti-tool-configuration'];
$this->assertEquals("12", $lti['deployment_id']);
$this->assertEquals($ltiorig['target_link_uri'], $lti['target_link_uri']);
$this->assertEquals($ltiorig['domain'], $lti['domain']);
$this->assertEquals($ltiorig['custom_parameters'], $lti['custom_parameters']);
$this->assertEquals($ltiorig['description'], $lti['description']);
$dlmsgorig = $ltiorig['messages'][0];
$dlmsg = $lti['messages'][0];
$this->assertEquals($dlmsgorig['type'], $dlmsg['type']);
$this->assertEquals($dlmsgorig['target_link_uri'], $dlmsg['target_link_uri']);
}
}