MDL-70500 lti: dyn reg can be used to update tools

This commit is contained in:
Claude Vervoort 2020-10-30 15:51:42 -04:00
parent d65ed58e73
commit ca80d53b38
14 changed files with 621 additions and 131 deletions

View File

@ -54,18 +54,29 @@
// 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 contains the OAuth 1.0a implementation used for support for LTI 1.1.
*
* @package mod_lti
* @copyright moodle
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace moodle\mod\lti;//Using a namespace as the basicLTI module imports classes with the same names
defined('MOODLE_INTERNAL') || die;
$oauth_last_computed_signature = false;
$lastcomputedsignature = false;
/* Generic exception class
/**
* Generic exception class
*/
class OAuthException extends \Exception {
// pass
}
/**
* OAuth 1.0 Consumer class
*/
class OAuthConsumer {
public $key;
public $secret;
@ -118,17 +129,25 @@ class OAuthSignatureMethod {
}
}
class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {
function get_name() {
return "HMAC-SHA1";
}
/**
* Base class for the HMac based signature methods.
*/
abstract class OAuthSignatureMethod_HMAC extends OAuthSignatureMethod {
/**
* Name of the Algorithm used.
*
* @return string algorithm name.
*/
abstract public function get_name(): string;
public function build_signature($request, $consumer, $token) {
global $oauth_last_computed_signature;
$oauth_last_computed_signature = false;
global $lastcomputedsignature;
$lastcomputedsignature = false;
$base_string = $request->get_signature_base_string();
$request->base_string = $base_string;
$basestring = $request->get_signature_base_string();
$request->base_string = $basestring;
$key_parts = array(
$consumer->secret,
@ -138,15 +157,48 @@ class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {
$key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
$key = implode('&', $key_parts);
$computed_signature = base64_encode(hash_hmac('sha1', $base_string, $key, true));
$oauth_last_computed_signature = $computed_signature;
return $computed_signature;
$computedsignature = base64_encode(hash_hmac(strtolower(substr($this->get_name(), 5)), $basestring, $key, true));
$lastcomputedsignature = $computedsignature;
return $computedsignature;
}
}
/**
* Implementation for SHA 1.
*/
class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod_HMAC {
/**
* Name of the Algorithm used.
*
* @return string algorithm name.
*/
public function get_name(): string {
return "HMAC-SHA1";
}
}
/**
* Implementation for SHA 256.
*/
class OAuthSignatureMethod_HMAC_SHA256 extends OAuthSignatureMethod_HMAC {
/**
* Name of the Algorithm used.
*
* @return string algorithm name.
*/
public function get_name(): string {
return "HMAC-SHA256";
}
}
class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {
public function get_name() {
/**
* Name of the Algorithm used.
*
* @return string algorithm name.
*/
public function get_name(): string {
return "PLAINTEXT";
}
@ -170,7 +222,12 @@ class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {
}
class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {
public function get_name() {
/**
* Name of the Algorithm used.
*
* @return string algorithm name.
*/
public function get_name(): string {
return "RSA-SHA1";
}
@ -539,8 +596,8 @@ class OAuthServer {
* verify an api call, checks all the parameters
*/
public function verify_request(&$request) {
global $oauth_last_computed_signature;
$oauth_last_computed_signature = false;
global $lastcomputedsignature;
$lastcomputedsignature = false;
$this->get_version($request);
$consumer = $this->get_consumer($request);
$token = $this->get_token($request, $consumer, "access");
@ -620,8 +677,8 @@ class OAuthServer {
*/
private function check_signature(&$request, $consumer, $token) {
// this should probably be in a different method
global $oauth_last_computed_signature;
$oauth_last_computed_signature = false;
global $lastcomputedsignature;
$lastcomputedsignature = false;
$timestamp = @ $request->get_parameter('oauth_timestamp');
$nonce = @ $request->get_parameter('oauth_nonce');
@ -636,8 +693,8 @@ class OAuthServer {
if (!$valid_sig) {
$ex_text = "Invalid signature";
if ($oauth_last_computed_signature) {
$ex_text = $ex_text . " ours= $oauth_last_computed_signature yours=$signature";
if ($lastcomputedsignature) {
$ex_text = $ex_text . " ours= $lastcomputedsignature yours=$signature";
}
throw new OAuthException($ex_text);
}

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",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=q().trim();if(b){a(j.TOOL_URL).val("");y();p(b)}},F=function(){var b=q().trim();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()}}});
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","core/config"],function(a,b,c,d,e,f,g,h,i,j){var k={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"},l=function(){return a(k.TOOL_LIST_CONTAINER)},m=function(){return a(k.EXTERNAL_REGISTRATION_CONTAINER)},n=function(){return a(k.CARTRIDGE_REGISTRATION_CONTAINER)},o=function(){return a(k.REGISTRATION_CHOICE_CONTAINER)},p=function(b){if(b.data&&"org.imsglobal.lti.close"===b.data.subject){a(k.EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER).empty();s();x();A();x();E()}},q=function(b){a(k.EXTERNAL_REGISTRATION_PAGE_CONTAINER).removeClass("hidden");var c=a(k.EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER);c.append(a("<iframe src='startltiadvregistration.php?url="+encodeURIComponent(b)+"&sesskey="+j.sesskey+"'></iframe>"));v();window.addEventListener("message",p,!1)},r=function(){return a(k.TOOL_URL).val()},s=function(){m().addClass("hidden")},t=function(){n().addClass("hidden")},u=function(){o().addClass("hidden")},v=function(){t();u();m().removeClass("hidden");y(m())},w=function(a){s();u();var b=n();b.find("input").val("");b.removeClass("hidden");b.find(k.CARTRIDGE_REGISTRATION_FORM).attr("data-cartridge-url",a);y(b)},x=function(){s();t();o().removeClass("hidden");y(o())},y=function(a){var b=a.children().detach();b.appendTo(a)},z=function(){l().addClass("hidden")},A=function(){l().removeClass("hidden")},B=function(a){var b=a.error?"error":"success";c.addNotification({message:a.message,type:b})},C=function(a){a.addClass("loading")},D=function(a){a.removeClass("loading")},E=function(){var b=a.Deferred(),e=l();C(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(){D(e)})},F=function(){var b=r().trim();if(b){a(k.TOOL_URL).val("");z();q(b)}},G=function(){var b=r().trim();if(""===b){return a.Deferred().resolve()}var d=a(k.TOOL_CREATE_LTILEGACY_BUTTON);C(d);var f=g.isCartridge(b);f.always(function(){D(d)});f.done(function(c){if(c.iscartridge){a(k.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},H=function(){a(document).on(e.NEW_TOOL_TYPE,function(){E()});a(document).on(e.START_EXTERNAL_REGISTRATION,function(){v();a(k.TOOL_URL).val("");z()});a(document).on(e.STOP_EXTERNAL_REGISTRATION,function(){A();x()});a(document).on(e.START_CARTRIDGE_REGISTRATION,function(a,b){w(b)});a(document).on(e.STOP_CARTRIDGE_REGISTRATION,function(){n().find(k.CARTRIDGE_REGISTRATION_FORM).removeAttr("data-cartridge-url");x()});a(document).on(e.REGISTRATION_FEEDBACK,function(a,b){B(b)});var b=a(k.TOOL_CREATE_LTILEGACY_BUTTON);b.click(function(a){a.preventDefault();G()});var c=a(k.TOOL_CREATE_BUTTON);c.click(function(a){a.preventDefault();F()})};return{init:function init(){H();E()}}});
//# sourceMappingURL=tool_configure_controller.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -26,8 +26,8 @@
* @since 3.1
*/
define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/events', 'mod_lti/keys', 'mod_lti/tool_type',
'mod_lti/tool_proxy', 'core/str'],
function($, ajax, notification, templates, ltiEvents, KEYS, toolType, toolProxy, str) {
'mod_lti/tool_proxy', 'core/str', 'core/config'],
function($, ajax, notification, templates, ltiEvents, KEYS, toolType, toolProxy, str, config) {
var SELECTORS = {
EXTERNAL_REGISTRATION_CONTAINER: '#external-registration-container',
@ -116,7 +116,7 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/e
$(SELECTORS.EXTERNAL_REGISTRATION_PAGE_CONTAINER).removeClass('hidden');
var container = $(SELECTORS.EXTERNAL_REGISTRATION_TEMPLATE_CONTAINER);
container.append($("<iframe src='startltiadvregistration.php?url="
+ encodeURIComponent(url) + "'></iframe>"));
+ encodeURIComponent(url) + "&sesskey=" + config.sesskey + "'></iframe>"));
showExternalRegistration();
window.addEventListener("message", closeLTIAdvRegistration, false);
};

View File

@ -51,6 +51,19 @@ class registration_helper {
/** Tool Settings scope */
const SCOPE_TOOL_SETTING = 'https://purl.imsglobal.org/spec/lti-ts/scope/toolsetting';
/** Indicates the token is to create a new registration */
const REG_TOKEN_OP_NEW_REG = 'reg';
/** Indicates the token is to update an existing registration */
const REG_TOKEN_OP_UPDATE_REG = 'reg-update';
/**
* Get an instance of this helper
*
* @return object
*/
public static function get() {
return new registration_helper();
}
/**
* Function used to validate parameters.
@ -64,7 +77,7 @@ class registration_helper {
*
* @return mixed
*/
private static function get_parameter(array $payload, string $key, bool $required) {
private 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);
@ -87,33 +100,36 @@ class registration_helper {
*
* @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);
public function registration_to_config(array $registrationpayload, string $clientid): object {
$responsetypes = $this->get_parameter($registrationpayload, 'response_types', true);
$initiateloginuri = $this->get_parameter($registrationpayload, 'initiate_login_uri', true);
$redirecturis = $this->get_parameter($registrationpayload, 'redirect_uris', true);
$clientname = $this->get_parameter($registrationpayload, 'client_name', true);
$jwksuri = $this->get_parameter($registrationpayload, 'jwks_uri', true);
$tokenendpointauthmethod = $this->get_parameter($registrationpayload, 'token_endpoint_auth_method', true);
$applicationtype = self::get_parameter($registrationpayload, 'application_type', false);
$logouri = self::get_parameter($registrationpayload, 'logo_uri', false);
$applicationtype = $this->get_parameter($registrationpayload, 'application_type', false);
$logouri = $this->get_parameter($registrationpayload, 'logo_uri', false);
$ltitoolconfiguration = self::get_parameter($registrationpayload,
$ltitoolconfiguration = $this->get_parameter($registrationpayload,
'https://purl.imsglobal.org/spec/lti-tool-configuration', true);
$domain = self::get_parameter($ltitoolconfiguration, 'domain', false);
$targetlinkuri = self::get_parameter($ltitoolconfiguration, 'target_link_uri', false);
$customparameters = self::get_parameter($ltitoolconfiguration, 'custom_parameters', false);
$scopes = explode(" ", self::get_parameter($registrationpayload, 'scope', false) ?? '');
$claims = self::get_parameter($ltitoolconfiguration, 'claims', false);
$domain = $this->get_parameter($ltitoolconfiguration, 'domain', false);
$targetlinkuri = $this->get_parameter($ltitoolconfiguration, 'target_link_uri', false);
$customparameters = $this->get_parameter($ltitoolconfiguration, 'custom_parameters', false);
$scopes = explode(" ", $this->get_parameter($registrationpayload, 'scope', false) ?? '');
$claims = $this->get_parameter($ltitoolconfiguration, 'claims', false);
$messages = $ltitoolconfiguration['messages'] ?? [];
$description = self::get_parameter($ltitoolconfiguration, 'description', false);
$description = $this->get_parameter($ltitoolconfiguration, 'description', false);
// Validate domain and target link.
if (empty($domain)) {
throw new registration_exception('missing_domain', 400);
}
$targetlinkuri = $targetlinkuri ?: 'https://'.$domain;
// Stripping www as this is ignored for domain matching.
$domain = lti_get_domain_from_url($domain);
if ($domain !== lti_get_domain_from_url($targetlinkuri)) {
throw new registration_exception('domain_targetlinkuri_mismatch', 400);
}
@ -237,72 +253,107 @@ class registration_helper {
return $config;
}
/**
* Adds to the config the LTI 1.1 key and sign it with the 1.1 secret.
*
* @param array $lticonfig reference to lticonfig to which to add the 1.1 OAuth info.
* @param string $key - LTI 1.1 OAuth Key
* @param string $secret - LTI 1.1 OAuth Secret
*
*/
private function add_previous_key_claim(array &$lticonfig, string $key, string $secret) {
if ($key) {
$oauthconsumer = [];
$oauthconsumer['key'] = $key;
$oauthconsumer['nonce'] = random_string(random_int(10, 20));
$oauthconsumer['sign'] = hash('sha256', $key.$secret.$oauthconsumer['nonce']);
$lticonfig['oauth_consumer'] = $oauthconsumer;
}
}
/**
* 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.
* @param object $type tool instance in case the tool already exists.
*
* @return array the Client Registration as an associative array.
*/
public static function config_to_registration(object $config, int $typeid): array {
public function config_to_registration(object $config, int $typeid, object $type = null): array {
$configarray = [];
foreach ((array)$config as $k => $v) {
if (substr($k, 0, 4) == 'lti_') {
$k = substr($k, 4);
}
$configarray[$k] = $v;
}
$config = (object) $configarray;
$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 = [];
$ltiversion = $type ? $type->ltiversion : $config->ltiversion;
$lticonfigurationresponse['version'] = $ltiversion;
if ($ltiversion === LTI_VERSION_1P3) {
$registrationresponse['client_id'] = $type ? $type->clientid : $config->clientid;
$registrationresponse['response_types'] = ['id_token'];
$registrationresponse['jwks_uri'] = $config->publickeyset;
$registrationresponse['initiate_login_uri'] = $config->initiatelogin;
$registrationresponse['grant_types'] = ['client_credentials', 'implicit'];
$registrationresponse['redirect_uris'] = explode(PHP_EOL, $config->redirectionuris);
$registrationresponse['application_type'] = 'web';
$registrationresponse['token_endpoint_auth_method'] = 'private_key_jwt';
} else if ($ltiversion === LTI_VERSION_1 && $type) {
$this->add_previous_key_claim($lticonfigurationresponse, $config->resourcekey, $config->password);
} else if ($ltiversion === LTI_VERSION_2 && $type) {
$toolproxy = $this->get_tool_proxy($type->toolproxyid);
$this->add_previous_key_claim($lticonfigurationresponse, $toolproxy['guid'], $toolproxy['secret']);
}
$registrationresponse['client_name'] = $type ? $type->name : $config->typename;
$registrationresponse['logo_uri'] = $type ? ($type->secureicon ?? $type->icon ?? '') : $config->icon ?? '';
$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) {
$lticonfigurationresponse['target_link_uri'] = $type ? $type->baseurl : $config->toolurl ?? '';
$lticonfigurationresponse['domain'] = $type ? $type->tooldomain : $config->tooldomain ?? '';
$lticonfigurationresponse['description'] = $type ? $type->description ?? '' : $config->description ?? '';
if ($config->contentitem ?? 0 == 1) {
$contentitemmessage = [];
$contentitemmessage['type'] = 'LtiDeepLinkingRequest';
if (isset($config->lti_toolurl_ContentItemSelectionRequest)) {
$contentitemmessage['target_link_uri'] = $config->lti_toolurl_ContentItemSelectionRequest;
if (isset($config->toolurl_ContentItemSelectionRequest)) {
$contentitemmessage['target_link_uri'] = $config->toolurl_ContentItemSelectionRequest;
}
$lticonfigurationresponse['messages'] = [$contentitemmessage];
}
if (isset($config->lti_customparameters) && !empty($config->lti_customparameters)) {
if (isset($config->customparameters) && !empty($config->customparameters)) {
$params = [];
foreach (explode(PHP_EOL, $config->lti_customparameters) as $param) {
foreach (explode(PHP_EOL, $config->customparameters) as $param) {
$split = explode('=', $param);
$params[$split[0]] = $split[1];
}
$lticonfigurationresponse['custom_parameters'] = $params;
}
$scopesresponse = [];
if ($config->ltiservice_gradesynchronization > 0) {
if ($config->ltiservice_gradesynchronization ?? 0 > 0) {
$scopesresponse[] = self::SCOPE_SCORE;
$scopesresponse[] = self::SCOPE_RESULT;
$scopesresponse[] = self::SCOPE_LINEITEM_RO;
}
if ($config->ltiservice_gradesynchronization == 2) {
if ($config->ltiservice_gradesynchronization ?? 0 == 2) {
$scopesresponse[] = self::SCOPE_LINEITEM;
}
if ($config->ltiservice_memberships == 1) {
if ($config->ltiservice_memberships ?? 0 == 1) {
$scopesresponse[] = self::SCOPE_NRPS;
}
if ($config->ltiservice_toolsettings == 1) {
if ($config->ltiservice_toolsettings ?? 0 == 1) {
$scopesresponse[] = self::SCOPE_TOOL_SETTING;
}
$registrationresponse['scope'] = implode(' ', $scopesresponse);
$claimsresponse = ['sub', 'iss'];
if ($config->lti_sendname == LTI_SETTING_ALWAYS) {
if ($config->sendname ?? '' == LTI_SETTING_ALWAYS) {
$claimsresponse[] = 'name';
$claimsresponse[] = 'family_name';
$claimsresponse[] = 'given_name';
}
if ($config->lti_sendemailaddr == LTI_SETTING_ALWAYS) {
if ($config->sendemailaddr ?? '' == LTI_SETTING_ALWAYS) {
$claimsresponse[] = 'email';
}
$lticonfigurationresponse['claims'] = $claimsresponse;
@ -316,21 +367,32 @@ class registration_helper {
*
* @param string $registrationtokenjwt registration token
*
* @return string client id for the registration
* @return array with 2 keys: clientid for the registration, type but only if it's an update
*/
public static function validate_registration_token(string $registrationtokenjwt): string {
public function validate_registration_token(string $registrationtokenjwt): array {
global $DB;
$keys = JWK::parseKeySet(jwks_helper::get_jwks());
$registrationtoken = JWT::decode($registrationtokenjwt, $keys, ['RS256']);
$response = [];
// 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);
if ($registrationtoken->scope == self::REG_TOKEN_OP_NEW_REG) {
// Checks if clientid is already registered.
if (!empty($DB->get_record('lti_types', array('clientid' => $clientid)))) {
throw new registration_exception("token_already_used", 401);
}
$response['clientid'] = $clientid;
} else if ($registrationtoken->scope == self::REG_TOKEN_OP_UPDATE_REG) {
$tool = lti_get_type($registrationtoken->sub);
if (!$tool) {
throw new registration_exception("Unknown client", 400);
}
$response['clientid'] = $tool->clientid ?? $this->new_clientid();
$response['type'] = $tool;
} else {
throw new registration_exception("Incorrect scope", 403);
}
return $clientid;
return $response;
}
/**
@ -338,7 +400,7 @@ class registration_helper {
*
* @return array List of scopes
*/
public static function lti_get_service_scopes() {
public function lti_get_service_scopes() {
$services = lti_get_services();
$scopes = array();
@ -351,4 +413,35 @@ class registration_helper {
return $scopes;
}
/**
* Generates a new client id string.
*
* @return string generated client id
*/
public function new_clientid(): string {
return random_string(15);
}
/**
* Base64 encoded signature for LTI 1.1 migration.
* @param string $key LTI 1.1 key
* @param string $salt Salt value
* @param string $secret LTI 1.1 secret
*
* @return string base64encoded hash
*/
public function sign(string $key, string $salt, string $secret): string {
return base64_encode(hash_hmac('sha-256', $key.$salt, $secret, true));
}
/**
* Returns a tool proxy
*
* @param int $proxyid
*
* @return mixed Tool Proxy details
*/
public function get_tool_proxy(int $proxyid) : array {
return lti_get_tool_proxy($proxyid);
}
}

View File

@ -0,0 +1,69 @@
<?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/>.
/**
* Class containing data for rendering LTI upgrade choices page.
*
* @copyright 2021 Cengage
* @package mod_lti
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_lti\output;
defined('MOODLE_INTERNAL') || die;
require_once($CFG->dirroot.'/mod/lti/locallib.php');
use renderable;
use templatable;
use renderer_base;
use stdClass;
/**
* Class containing data for rendering LTI upgrade choices page.
*
* @copyright 2021 Cengage
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class registration_upgrade_choice_page implements renderable, templatable {
/**
* Constructor
*
* @param array $tools array of tools that can be upgraded
* @param string $startregurl tool URL to start the registration process
*/
public function __construct(array $tools, string $startregurl) {
$this->tools = $tools;
$this->startregurl = $startregurl;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output The renderer
* @return stdClass Data to be used by the template
*/
public function export_for_template(renderer_base $output) {
$renderdata = new stdClass();
$renderdata->startregurlenc = urlencode($this->startregurl);
$renderdata->sesskey = sesskey();
$renderdata->tools = [];
foreach ($this->tools as $tool) {
$renderdata->tools[] = (object)$tool;
}
return $renderdata;
}
}

View File

@ -59,4 +59,16 @@ class renderer extends plugin_renderer_base {
$data = $page->export_for_template($this);
return parent::render_from_template('mod_lti/external_registration_return', $data);
}
/**
* Render the external registration return page
*
* @param tool_configure_page $page
*
* @return string html for the page
*/
public function render_registration_upgrade_choice_page($page) {
$data = $page->export_for_template($this);
return parent::render_from_template('mod_lti/registration_upgrade_choice_page', $data);
}
}

View File

@ -166,6 +166,15 @@ $string['domain_mismatch'] = 'Tool URL\'s domain does not match tool configurati
$string['donot'] = 'Do not send';
$string['donotaccept'] = 'Do not accept';
$string['donotallow'] = 'Do not allow';
$string['dynreg_update_text'] = 'There are existing tools attached to the registration\'s domain. Do you want to update an already installed
external tool or create a new external tool?';
$string['dynreg_update_warn_dupdomain'] = 'It is not recommended to have multiple external tools under the same domain.';
$string['dynreg_update_name'] = 'Tool name';
$string['dynreg_update_url'] = 'Base URL';
$string['dynreg_update_version'] = 'LTI Version';
$string['dynreg_update_notools'] = 'No tools in context.';
$string['dynreg_update_btn_update'] = 'Update';
$string['dynreg_update_btn_new'] = 'Register as a new external tool';
$string['duplicateregurl'] = 'This registration URL is already in use';
$string['editdescription'] = 'Click here to give this tool a description';
$string['edittype'] = 'Edit preconfigured tool';

View File

@ -55,6 +55,7 @@ use moodle\mod\lti as lti;
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
use mod_lti\local\ltiopenid\jwks_helper;
use mod_lti\local\ltiopenid\registration_helper;
global $CFG;
require_once($CFG->dirroot.'/mod/lti/OAuth.php');
@ -2752,7 +2753,7 @@ function lti_prepare_type_for_save($type, $config) {
$type->clientid = $config->lti_clientid;
}
if ((!empty($type->ltiversion) && $type->ltiversion === LTI_VERSION_1P3) && empty($type->clientid)) {
$type->clientid = random_string(15);
$type->clientid = registration_helper::get()->new_clientid();
} else if (empty($type->clientid)) {
$type->clientid = null;
}
@ -2822,6 +2823,14 @@ function lti_update_type($type, $config) {
lti_update_config($record);
}
}
if (isset($type->toolproxyid) && $type->ltiversion === LTI_VERSION_1P3) {
// We need to remove the tool proxy for this tool to function under 1.3.
$toolproxyid = $type->toolproxyid;
$DB->delete_records('lti_tool_settings', array('toolproxyid' => $toolproxyid));
$DB->delete_records('lti_tool_proxies', array('id' => $toolproxyid));
$type->toolproxyid = null;
$DB->update_record('lti_types', $type);
}
require_once($CFG->libdir.'/modinfolib.php');
if ($clearcache) {
$sql = "SELECT DISTINCT course

View File

@ -33,7 +33,7 @@ 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 = registration_helper::get()->lti_get_service_scopes();
$scopes[] = 'openid';
$conf = [
'issuer' => $CFG->wwwroot,

View File

@ -32,27 +32,50 @@ 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);
if ($_SERVER['REQUEST_METHOD'] === 'POST' or ($_SERVER['REQUEST_METHOD'] === 'GET')) {
$doregister = $_SERVER['REQUEST_METHOD'] === 'POST';
// 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 {
// 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();
// Registers tool.
try {
$tokenres = registration_helper::get()->validate_registration_token(trim(substr($authheader, 7)));
$type = new stdClass();
$type->state = LTI_TOOL_STATE_PENDING;
if (array_key_exists('type', $tokenres)) {
$type = $tokenres['type'];
}
if ($doregister) {
$registrationpayload = json_decode(file_get_contents('php://input'), true);
$config = registration_helper::get()->registration_to_config($registrationpayload, $tokenres['clientid']);
if ($type->id) {
lti_update_type($type, clone $config);
$typeid = $type->id;
} else {
$typeid = lti_add_type($type, clone $config);
}
header('Content-Type: application/json; charset=utf-8');
$message = json_encode(registration_helper::get()->config_to_registration((object)$config, $typeid));
} else if ($type) {
$config = lti_get_type_config($type->id);
header('Content-Type: application/json; charset=utf-8');
$message = json_encode(registration_helper::get()->config_to_registration((object)$config, $type->id, $type));
} else {
$code = 404;
$message = "No registration found.";
}
} catch (registration_exception $e) {
$code = $e->getCode();
$message = $e->getMessage();
}
}
} else {
$code = 400;
$message = 'Unsupported operation';
}
$response = new \mod_lti\local\ltiservice\response();
// Set code.

View File

@ -26,26 +26,55 @@
use Firebase\JWT\JWT;
use mod_lti\local\ltiopenid\jwks_helper;
use mod_lti\local\ltiopenid\registration_helper;
require_once(__DIR__ . '/../../config.php');
require_once($CFG->libdir.'/weblib.php');
require_once($CFG->dirroot . '/mod/lti/locallib.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));
$typeid = optional_param('type', -1, PARAM_INT);
$types = lti_get_tools_by_url($starturl, null);
if (!empty($types) && $typeid == -1) {
// There are matching types for the registration domain, let's prompt the user to upgrade.
$pageurl = new moodle_url('/mod/lti/startltiadvregistration.php');
$PAGE->set_context($context);
$PAGE->set_url($pageurl);
$PAGE->set_pagelayout('maintenance');
$output = $PAGE->get_renderer('mod_lti');
$page = new \mod_lti\output\registration_upgrade_choice_page($types, $starturl);
echo $output->header();
echo $output->render($page);
echo $output->footer();
} else {
// Let's actually start the registration process by launching the tool registration
// endpoint with the registration token and the site config url.
require_sesskey();
$sub = registration_helper::get()->new_clientid();
$scope = registration_helper::REG_TOKEN_OP_NEW_REG;
if ($typeid > 0) {
// In the context of an update, the sub is the id of the type.
$sub = strval($typeid);
$scope = registration_helper::REG_TOKEN_OP_UPDATE_REG;
}
$now = time();
$token = [
"sub" => $sub,
"scope" => $scope,
"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

@ -0,0 +1,94 @@
{{!
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/>.
}}
{{!
@template mod_lti/registration_upgrade_choice_page.mustache
This template provides the layout to display the upgrade choices
available when registering a tool for which there exists at least
one other tool with the same domain.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* startregurlenc: the URL provided by the tool to start the registration, url encoded
* tools: the list of tools matching the domain
Example context (json):
{
"startregurlenc":"https%3A%2F%2Fmytool.example%2Fregister",
"tools": [
{
"id": 222,
"name": "This is My LTI 1.1 tool",
"baseurl": "https://mytool.example",
"version": "LTI-1p0",
"sesskey": "785t9302"
}
]
}
}}
<p>{{#str}}dynreg_update_text, mod_lti {{/str}}</p>
<div class="alert alert-warning" role="alert">
{{#str}}dynreg_update_warn_dupdomain, mod_lti {{/str}}
</div>
<table class="table table-striped">
<tr>
<th>{{#str}}dynreg_update_name, mod_lti {{/str}}</th>
<th>{{#str}}dynreg_update_url, mod_lti {{/str}}</th>
<th>{{#str}}dynreg_update_version, mod_lti {{/str}}</th>
<th></th>
</tr>
{{#tools}}
<tr>
<td>{{name}}</td>
<td>{{baseurl}}</td>
<td>{{ltiversion}}</td>
<td><button data-href="startltiadvregistration.php?url={{startregurlenc}}&type={{id}}" class="btn btn-outline-primary">
<span class="btn-loader">{{> mod_lti/loader }}</span>
{{#str}}dynreg_update_btn_update, mod_lti {{/str}}
</button>
</td>
</tr>
{{/tools}}
</table>
{{^tools}}
{{#str}}dynreg_update_notools, mod_lti {{/str}}
{{/tools}}
<div>
<button data-href="startltiadvregistration.php?url={{startregurlenc}}&type=0" class="btn btn-primary">
<span class="btn-loader">{{> mod_lti/loader }}</span>
{{#str}}dynreg_update_btn_new, mod_lti {{/str}}
</button>
</div>
<form method="POST" id="startregform">
<input type="hidden" name="sesskey" value="{{sesskey}}"/>
</form>
{{#js}}
disableAndGo = e => {
document.querySelectorAll("button").forEach(e=>e.setAttribute("disabled", true));
e.target.querySelector("span.btn-loader").style.display="inline";
let form = document.getElementById("startregform");
form.action = e.target.getAttribute("data-href");
form.submit();
};
document.querySelectorAll("button").forEach(b=>b.onclick=disableAndGo);
{{/js}}

View File

@ -40,14 +40,14 @@
* @author Claude Vervoort
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_lti\local\ltiopenid;
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 {
class mod_lti_openidregistrationlib_testcase extends \advanced_testcase {
/**
* @var string A has-it-all client registration.
@ -108,8 +108,8 @@ EOD;
"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"
"domain": "www.example.org",
"target_link_uri": "https://www.example.org/lti"
}
}
EOD;
@ -146,7 +146,7 @@ EOD;
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');
$config = registration_helper::get()->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);
@ -175,12 +175,15 @@ EOD;
*/
public function test_to_config_minimal() {
$registration = json_decode($this->registrationminimaljson, true);
$config = registration_helper::registration_to_config($registration, 'TheClientId');
$config = registration_helper::get()->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);
// Special case here where Moodle ignores www for domains.
$this->assertEquals('example.org', $config->lti_tooldomain);
$this->assertEquals('https://www.example.org/lti', $config->lti_toolurl);
$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);
@ -200,7 +203,7 @@ EOD;
*/
public function test_to_config_minimal_with_deeplinking() {
$registration = json_decode($this->registrationminimaldljson, true);
$config = registration_helper::registration_to_config($registration, 'TheClientId');
$config = registration_helper::get()->registration_to_config($registration, 'TheClientId');
$this->assertEquals(1, $config->lti_contentitem);
$this->assertEmpty($config->lti_toolurl_ContentItemSelectionRequest);
}
@ -213,7 +216,7 @@ EOD;
$this->expectException(registration_exception::class);
$this->expectExceptionCode(400);
unset($registration['initiate_login_uri']);
registration_helper::registration_to_config($registration, 'TheClientId');
registration_helper::get()->registration_to_config($registration, 'TheClientId');
}
/**
@ -224,7 +227,7 @@ EOD;
$this->expectException(registration_exception::class);
$this->expectExceptionCode(400);
unset($registration['redirect_uris']);
registration_helper::registration_to_config($registration, 'TheClientId');
registration_helper::get()->registration_to_config($registration, 'TheClientId');
}
/**
@ -235,7 +238,7 @@ EOD;
$this->expectException(registration_exception::class);
$this->expectExceptionCode(400);
$registration['jwks_uri'] = '';
registration_helper::registration_to_config($registration, 'TheClientId');
registration_helper::get()->registration_to_config($registration, 'TheClientId');
}
/**
@ -247,7 +250,7 @@ EOD;
$this->expectExceptionCode(400);
unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['domain']);
unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['target_link_uri']);
registration_helper::registration_to_config($registration, 'TheClientId');
registration_helper::get()->registration_to_config($registration, 'TheClientId');
}
/**
@ -258,7 +261,7 @@ EOD;
$this->expectException(registration_exception::class);
$this->expectExceptionCode(400);
$registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['domain'] = 'not.the.right.domain';
registration_helper::registration_to_config($registration, 'TheClientId');
registration_helper::get()->registration_to_config($registration, 'TheClientId');
}
/**
@ -269,7 +272,7 @@ EOD;
unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['domain']);
$this->expectException(registration_exception::class);
$this->expectExceptionCode(400);
$config = registration_helper::registration_to_config($registration, 'TheClientId');
$config = registration_helper::get()->registration_to_config($registration, 'TheClientId');
}
/**
@ -278,9 +281,9 @@ EOD;
public function test_validation_domain_targetlinkuri_onlydomain() {
$registration = json_decode($this->registrationminimaljson, true);
unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['target_link_uri']);
$config = registration_helper::registration_to_config($registration, 'TheClientId');
$this->assertEquals('client.example.org', $config->lti_tooldomain);
$this->assertEquals('https://client.example.org', $config->lti_toolurl);
$config = registration_helper::get()->registration_to_config($registration, 'TheClientId');
$this->assertEquals('example.org', $config->lti_tooldomain);
$this->assertEquals('https://www.example.org', $config->lti_toolurl);
}
/**
@ -289,7 +292,8 @@ EOD;
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);
$reghelper = registration_helper::get();
$reg = $reghelper->config_to_registration($reghelper->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']);
@ -325,7 +329,8 @@ EOD;
*/
public function test_config_to_registration_minimal() {
$orig = json_decode($this->registrationminimaljson, true);
$reg = registration_helper::config_to_registration(registration_helper::registration_to_config($orig, 'clid'), 12);
$reghelper = registration_helper::get();
$reg = $reghelper->config_to_registration($reghelper->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']);
@ -342,4 +347,94 @@ EOD;
$this->assertFalse(in_array('name', $lti['claims']));
}
/**
* Test the transformation from lti config 1.1 to Registration Response.
*/
public function test_config_to_registration_lti11() {
$config = [];
$config['contentitem'] = 1;
$config['toolurl_ContentItemSelectionRequest'] = '';
$config['sendname'] = 0;
$config['sendemailaddr'] = 1;
$config['acceptgrades'] = 2;
$config['resourcekey'] = 'testkey';
$config['password'] = 'testp@ssw0rd';
$config['customparameters'] = 'a1=b1';
$type = [];
$type['id'] = 130;
$type['name'] = 'LTI Test 1.1';
$type['baseurl'] = 'https://base.test.url/test';
$type['tooldomain'] = 'base.test.url';
$type['ltiversion'] = 'LTI-1p0';
$type['icon'] = 'https://base.test.url/icon.png';
$reg = registration_helper::get()->config_to_registration((object)$config, $type['id'], (object)$type);
$this->assertFalse(isset($reg['client_id']));
$this->assertFalse(isset($reg['initiate_login_uri']));
$this->assertEquals($type['name'], $reg['client_name']);
$lti = $reg['https://purl.imsglobal.org/spec/lti-tool-configuration'];
$this->assertEquals(LTI_VERSION_1, $lti['version']);
$this->assertEquals('b1', $lti['custom_parameters']['a1']);
$this->assertEquals('LtiDeepLinkingRequest', $lti['messages'][0]['type']);
$this->assertEquals('base.test.url', $lti['domain']);
$this->assertEquals($type['baseurl'], $lti['target_link_uri']);
$oauth = $lti['oauth_consumer'];
$this->assertEquals('testkey', $oauth['key']);
$this->assertFalse(empty($oauth['nonce']));
$this->assertEquals(hash('sha256', 'testkeytestp@ssw0rd'.$oauth['nonce']), $oauth['sign']);
$this->assertTrue(in_array('iss', $lti['claims']));
$this->assertTrue(in_array('sub', $lti['claims']));
$this->assertTrue(in_array('email', $lti['claims']));
$this->assertFalse(in_array('family_name', $lti['claims']));
$this->assertFalse(in_array('given_name', $lti['claims']));
$this->assertFalse(in_array('name', $lti['claims']));
}
/**
* Test the transformation from lti config 2.0 to Registration Response.
* For LTI 2.0 we limit to just passing the previous key/secret.
*/
public function test_config_to_registration_lti20() {
$config = [];
$config['contentitem'] = 1;
$config['toolurl_ContentItemSelectionRequest'] = '';
$type = [];
$type['id'] = 131;
$type['name'] = 'LTI Test 1.2';
$type['baseurl'] = 'https://base.test.url/test';
$type['tooldomain'] = 'base.test.url';
$type['ltiversion'] = 'LTI-2p0';
$type['icon'] = 'https://base.test.url/icon.png';
$type['toolproxyid'] = 9;
$toolproxy = [];
$toolproxy['id'] = 9;
$toolproxy['guid'] = 'lti2guidtest';
$toolproxy['secret'] = 'peM7YDx420bo';
$reghelper = $this->getMockBuilder(registration_helper::class)
->setMethods(['get_tool_proxy'])
->getMock();
$map = [[$toolproxy['id'], $toolproxy]];
$reghelper->method('get_tool_proxy')
->will($this->returnValueMap($map));
$reg = $reghelper->config_to_registration((object)$config, $type['id'], (object)$type);
$this->assertFalse(isset($reg['client_id']));
$this->assertFalse(isset($reg['initiate_login_uri']));
$this->assertEquals($type['name'], $reg['client_name']);
$lti = $reg['https://purl.imsglobal.org/spec/lti-tool-configuration'];
$this->assertEquals(LTI_VERSION_2, $lti['version']);
$this->assertEquals('LtiDeepLinkingRequest', $lti['messages'][0]['type']);
$this->assertEquals('base.test.url', $lti['domain']);
$this->assertEquals($type['baseurl'], $lti['target_link_uri']);
$oauth = $lti['oauth_consumer'];
$this->assertEquals('lti2guidtest', $toolproxy['guid']);
$this->assertFalse(empty($oauth['nonce']));
$this->assertEquals(hash('sha256', 'lti2guidtestpeM7YDx420bo'.$oauth['nonce']), $oauth['sign']);
$this->assertTrue(in_array('iss', $lti['claims']));
$this->assertTrue(in_array('sub', $lti['claims']));
$this->assertFalse(in_array('email', $lti['claims']));
$this->assertFalse(in_array('family_name', $lti['claims']));
$this->assertFalse(in_array('given_name', $lti['claims']));
$this->assertFalse(in_array('name', $lti['claims']));
}
}