diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index e84c049b5a8..4eaa74220b8 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1741,6 +1741,10 @@ class core_plugin_manager { 'gregorian' ), + 'customfield' => array( + 'checkbox', 'date', 'select', 'text', 'textarea' + ), + 'coursereport' => array( // Deprecated! ), diff --git a/mod/lti/OAuthBody.php b/mod/lti/OAuthBody.php index 761b01c1618..147b695bb81 100644 --- a/mod/lti/OAuthBody.php +++ b/mod/lti/OAuthBody.php @@ -44,7 +44,8 @@ require_once($CFG->dirroot . '/mod/lti/TrivialStore.php'); * @param int $typeid LTI type ID. * @param string[] $scopes Array of scopes which give permission for the current request. * - * @return string|int|boolean The OAuth consumer key, the LTI type ID for the validated bearer token, otherwise false. + * @return string|int|boolean The OAuth consumer key, the LTI type ID for the validated bearer token, + true for requests not requiring a scope, otherwise false. */ function get_oauth_key_from_headers($typeid = null, $scopes = null) { global $DB; diff --git a/mod/lti/auth.php b/mod/lti/auth.php index 6f4da8a1db7..68c5ccdeb69 100644 --- a/mod/lti/auth.php +++ b/mod/lti/auth.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * This file returns an array of available public keys + * This file responds to a login authentication request * * @package mod_lti * @copyright 2019 Stephen Vickers @@ -37,7 +37,7 @@ $nonce = optional_param('nonce', '', PARAM_TEXT); $prompt = optional_param('prompt', '', PARAM_TEXT); $ok = !empty($scope) && !empty($responsetype) && !empty($clientid) && - !empty($redirecturi) && !empty($loginhint) && !empty($scope) && + !empty($redirecturi) && !empty($loginhint) && !empty($nonce) && !empty($SESSION->lti_message_hint); if (!$ok) { @@ -84,7 +84,9 @@ if ($ok) { $desc = 'Invalid response_mode'; } } else { - $responsemode = 'query'; + $ok = false; + $error = 'invalid_request'; + $desc = 'Missing response_mode'; } } if ($ok && !empty($prompt) && ($prompt !== 'none')) { @@ -132,24 +134,19 @@ if (isset($state)) { $params['state'] = $state; } unset($SESSION->lti_message_hint); -if ($responsemode !== 'query') { - $r = '
\n"; - if (!empty($params)) { - foreach ($params as $key => $value) { - $key = htmlspecialchars($key); - $value = htmlspecialchars($value); - $r .= " \n"; - } +$r = '\n"; +if (!empty($params)) { + foreach ($params as $key => $value) { + $key = htmlspecialchars($key); + $value = htmlspecialchars($value); + $r .= " \n"; } - $r .= "
\n"; - $r .= "\n"; - echo $r; -} else { - $url = new \moodle_url(redirecturi, $params); - redirect($url->out(false)); } +$r .= "\n"; +$r .= "\n"; +echo $r; diff --git a/mod/lti/backup/moodle2/backup_lti_stepslib.php b/mod/lti/backup/moodle2/backup_lti_stepslib.php index 7b4b45760c4..eae88a71bf4 100644 --- a/mod/lti/backup/moodle2/backup_lti_stepslib.php +++ b/mod/lti/backup/moodle2/backup_lti_stepslib.php @@ -99,6 +99,8 @@ class backup_lti_activity_structure_step extends backup_activity_structure_step 'state', 'course', 'coursevisible', + 'ltiversion', + 'clientid', 'toolproxyid', 'enabledcapability', 'parameter', diff --git a/mod/lti/classes/local/ltiservice/service_base.php b/mod/lti/classes/local/ltiservice/service_base.php index c896c6a6a20..4e30db0b16e 100644 --- a/mod/lti/classes/local/ltiservice/service_base.php +++ b/mod/lti/classes/local/ltiservice/service_base.php @@ -375,11 +375,7 @@ abstract class service_base { $key = $this->toolproxy->guid; $secret = $this->toolproxy->secret; } else { - if ($this->type->ltiversion === LTI_VERSION_1P3) { - $key = $this->type->clientid; // Should not be needed as access tokens are used. - } else { - $key = $this->typeconfig['resourcekey']; - } + $key = $this->typeconfig['resourcekey']; $secret = $this->typeconfig['password']; } if (!$this->is_unsigned() && ($key == $consumerkey)) { diff --git a/mod/lti/contentitem.php b/mod/lti/contentitem.php index 0c954949782..2528cba1c65 100644 --- a/mod/lti/contentitem.php +++ b/mod/lti/contentitem.php @@ -35,7 +35,7 @@ $text = optional_param('text', '', PARAM_RAW); $config = lti_get_type_type_config($id); if ($config->lti_ltiversion === LTI_VERSION_1P3) { if (!isset($SESSION->lti_initiatelogin_status)) { - echo lti_initiatelogin($courseid, 0, null, $config, 'ContentItemSelectionRequest', $title, $text); + echo lti_initiate_login($courseid, 0, null, $config, 'ContentItemSelectionRequest', $title, $text); exit; } else { unset($SESSION->lti_initiatelogin_status); diff --git a/mod/lti/contentitem_return.php b/mod/lti/contentitem_return.php index a6a1b7dfc00..cc29ae7049a 100644 --- a/mod/lti/contentitem_return.php +++ b/mod/lti/contentitem_return.php @@ -33,12 +33,12 @@ $jwt = optional_param('JWT', '', PARAM_RAW); if (!empty($jwt)) { list($tool, $params) = lti_convert_from_jwt($id, $jwt); - $consumerkey = isset($params['oauth_consumer_key']) ? $params['oauth_consumer_key'] : ''; - $messagetype = isset($params['lti_message_type']) ? $params['lti_message_type'] : ''; - $version = isset($params['lti_version']) ? $params['lti_version'] : ''; - $items = isset($params['content_items']) ? $params['content_items'] : ''; - $errormsg = isset($params['lti_errormsg']) ? $params['lti_errormsg'] : ''; - $msg = isset($params['lti_msg']) ? $params['lti_msg'] : ''; + $consumerkey = $params['oauth_consumer_key'] ?? ''; + $messagetype = $params['lti_message_type'] ?? ''; + $version = $params['lti_version'] ?? ''; + $items = $params['content_items'] ?? ''; + $errormsg = $params['lti_errormsg'] ?? ''; + $msg = $params['lti_msg'] ?? ''; } else { $consumerkey = required_param('oauth_consumer_key', PARAM_RAW); $messagetype = required_param('lti_message_type', PARAM_TEXT); @@ -60,7 +60,7 @@ $redirecturl = null; $returndata = null; if (empty($errormsg) && !empty($items)) { try { - $returndata = lti_tool_configuration_from_content_item($tool, $messagetype, $version, $consumerkey, $items); + $returndata = lti_tool_configuration_from_content_item($id, $messagetype, $version, $consumerkey, $items); } catch (moodle_exception $e) { $errormsg = $e->getMessage(); } diff --git a/mod/lti/launch.php b/mod/lti/launch.php index 20b04b455e7..6da25d532f5 100644 --- a/mod/lti/launch.php +++ b/mod/lti/launch.php @@ -61,7 +61,7 @@ if ($typeid) { $config = lti_get_type_type_config($typeid); if ($config->lti_ltiversion === LTI_VERSION_1P3) { if (!isset($SESSION->lti_initiatelogin_status)) { - echo lti_initiatelogin($cm->course, $id, $lti, $config); + echo lti_initiate_login($cm->course, $id, $lti, $config); exit; } else { unset($SESSION->lti_initiatelogin_status); diff --git a/mod/lti/locallib.php b/mod/lti/locallib.php index 1c13c275b7d..1ea11a9e878 100644 --- a/mod/lti/locallib.php +++ b/mod/lti/locallib.php @@ -496,7 +496,7 @@ function lti_get_jwt_claim_mapping() { * Return the launch data required for opening the external tool. * * @param stdClass $instance the external tool activity settings - * @param string $nonce + * @param string $nonce the nonce value to use (applies to LTI 1.3 only) * @return array the endpoint URL and parameters (including the signature) * @since Moodle 3.0 */ @@ -666,7 +666,11 @@ function lti_get_launch_data($instance, $nonce = '') { } if ((!empty($key) && !empty($secret)) || ($ltiversion === LTI_VERSION_1P3)) { - $parms = lti_sign_parameters($requestparams, $endpoint, "POST", $key, $secret, $typeid, $nonce); + if ($ltiversion !== LTI_VERSION_1P3) { + $parms = lti_sign_parameters($requestparams, $endpoint, 'POST', $key, $secret); + } else { + $parms = lti_sign_jwt($requestparams, $endpoint, $key, $typeid, $nonce); + } $endpointurl = new \moodle_url($endpoint); $endpointparams = $endpointurl->params(); @@ -926,6 +930,8 @@ function lti_build_request_lti2($tool, $params) { * @param string $messagetype The request message type. Defaults to basic-lti-launch-request if empty. * * @return array Request details + * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. + * @see lti_build_standard_message() */ function lti_build_standard_request($instance, $orgid, $islti2, $messagetype = 'basic-lti-launch-request') { if (!$islti2) { @@ -1057,7 +1063,7 @@ function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $cus function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [], $presentationtargets = [], $autocreate = false, $multiple = false, $unsigned = false, $canconfirm = false, $copyadvice = false, $nonce = '') { - global $PAGE, $USER; + global $USER; $tool = lti_get_type($id); // Validate parameters. @@ -1157,7 +1163,7 @@ function lti_build_content_item_selection_request($id, $course, moodle_url $retu $services = lti_get_services(); foreach ($services as $service) { $serviceparameters = $service->get_launch_parameters('ContentItemSelectionRequest', - $PAGE->course->id, $USER->id , $id); + $course->id, $USER->id , $id); foreach ($serviceparameters as $paramkey => $paramvalue) { $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue, $islti2); @@ -1207,7 +1213,11 @@ function lti_build_content_item_selection_request($id, $course, moodle_url $retu $requestparams['content_item_return_url'] = $returnurl->out(false); $requestparams['title'] = $title; $requestparams['text'] = $text; - $signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret, $id, $nonce); + if (!$islti13) { + $signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret); + } else { + $signedparams = lti_sign_jwt($requestparams, $toolurlout, $key, $id, $nonce); + } $toolurlparams = $toolurl->params(); // Strip querystring params in endpoint url from $signedparams to avoid duplication. @@ -1259,12 +1269,10 @@ function lti_verify_oauth_signature($typeid, $consumerkey) { $typeconfig = lti_get_type_config($typeid); if (isset($tool->toolproxyid)) { - $islti2 = true; $toolproxy = lti_get_tool_proxy($tool->toolproxyid); $key = $toolproxy->guid; $secret = $toolproxy->secret; } else { - $islti2 = false; $toolproxy = null; if (!empty($typeconfig['resourcekey'])) { $key = $typeconfig['resourcekey']; @@ -1306,6 +1314,11 @@ function lti_verify_oauth_signature($typeid, $consumerkey) { * * @return stdClass Tool type * @throws moodle_exception + * @throws UnexpectedValueException Provided JWT was invalid + * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed + * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' + * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' + * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim */ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) { $tool = lti_get_type($typeid); @@ -1313,22 +1326,15 @@ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) { if (!$tool) { throw new moodle_exception('errortooltypenotfound', 'mod_lti'); } - $typeconfig = lti_get_type_config($typeid); - if (isset($tool->toolproxyid)) { throw new moodle_exception('JWT security not supported with LTI 2'); - } else { - if (!empty($tool->clientid)) { - $key = $tool->clientid; - } else { - $key = ''; - } - if (!empty($typeconfig['publickey'])) { - $publickey = $typeconfig['publickey']; - } else { - $publickey = ''; - } } + + $typeconfig = lti_get_type_config($typeid); + + $key = $tool->clientid ?? ''; + $publickey = $typeconfig['publickey'] ?? ''; + if ($consumerkey !== $key) { throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti'); } @@ -1336,11 +1342,7 @@ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) { throw new moodle_exception('No public key configured'); } - try { - $jwt = JWT::decode($jwtparam, $publickey, array('RS256')); - } catch (Exception $e) { - throw new moodle_exception($e->getMessage()); - } + JWT::decode($jwtparam, $publickey, array('RS256')); return $tool; } @@ -1349,7 +1351,7 @@ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) { * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the * selected content item. This configuration data can be then used when adding a tool into the course. * - * @param stdClass $tool The tool type. + * @param int $typeid The tool type ID. * @param string $messagetype The value for the lti_message_type parameter. * @param string $ltiversion The value for the lti_version parameter. * @param string $consumerkey The consumer key. @@ -1358,7 +1360,12 @@ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) { * @throws moodle_exception * @throws lti\OAuthException */ -function lti_tool_configuration_from_content_item($tool, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) { +function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) { + $tool = lti_get_type($typeid); + // Validate parameters. + if (!$tool) { + throw new moodle_exception('errortooltypenotfound', 'mod_lti'); + } // Check lti_message_type. Show debugging if it's not set to ContentItemSelection. // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment. if ($messagetype !== 'ContentItemSelection') { @@ -1473,6 +1480,7 @@ function lti_tool_configuration_from_content_item($tool, $messagetype, $ltiversi * Converts the new Deep-Linking format for Content-Items to the old format. * * @param string $param JSON string representing new Deep-Linking format + * @return string JSON representation of content-items */ function lti_convert_content_items($param) { $items = array(); @@ -2984,24 +2992,39 @@ function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $insta * @param string $oauthconsumersecret * @return array|null */ -function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret, $typeid = 0, $nonce = '') { +function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) { $parms = $oldparms; $testtoken = ''; - if ($parms['lti_version'] !== LTI_VERSION_1P3) { - // TODO: Switch to core oauthlib once implemented - MDL-30149. - $hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1(); - $testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null); - $accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms); - $accreq->sign_request($hmacmethod, $testconsumer, $testtoken); + // TODO: Switch to core oauthlib once implemented - MDL-30149. + $hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1(); + $testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null); + $accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms); + $accreq->sign_request($hmacmethod, $testconsumer, $testtoken); - $newparms = $accreq->get_parameters(); - } else { - $newparms = array(); - $newparms['id_token'] = lti_convert_to_jwt($parms, $endpoint, $oauthconsumerkey, $typeid, $nonce); - } + $newparms = $accreq->get_parameters(); + + return $newparms; +} + +/** + * Signs the petition to launch the external tool using JWT + * + * @param array $oldparms Parameters to be passed for signing + * @param string $endpoint url of the external tool + * @param string $oauthconsumerkey + * @param string $typeid ID of LTI tool type + * @param string $nonce Nonce value to use + * @return array|null + */ +function lti_sign_jwt($oldparms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce = '') { + + $parms = $oldparms; + + $newparms = array(); + $newparms['id_token'] = lti_convert_to_jwt($parms, $endpoint, $oauthconsumerkey, $typeid, $nonce); return $newparms; } @@ -3098,11 +3121,12 @@ function lti_convert_to_jwt($parms, $endpoint, $oauthconsumerkey, $typeid, $nonc * @param int $typeid * @param string $jwtparam JWT parameter * - * @return stdClass Tool type + * @return array containing tool type and the array of parameters * @throws moodle_exception */ function lti_convert_from_jwt($typeid, $jwtparam) { + $params = array(); $parts = explode('.', $jwtparam); $ok = (count($parts) === 3); if ($ok) { @@ -3112,75 +3136,59 @@ function lti_convert_from_jwt($typeid, $jwtparam) { } if ($ok) { $tool = lti_verify_jwt_signature($typeid, $claims['iss'], $jwtparam); - } - $params = array(); - $params['oauth_consumer_key'] = $claims['iss']; - foreach (lti_get_jwt_claim_mapping() as $key => $mapping) { - $claim = LTI_JWT_CLAIM_PREFIX; - if (!empty($mapping['suffix'])) { - $claim .= "-{$mapping['suffix']}"; - } - $claim .= '/claim/'; - if (is_null($mapping['group'])) { - $claim = $mapping['claim']; - } else if (empty($mapping['group'])) { - $claim .= $mapping['claim']; - } else { - $claim .= $mapping['group']; - } - if (isset($claims[$claim])) { - $value = null; - if (empty($mapping['group'])) { - $value = $claims[$claim]; - } else { - $group = $claims[$claim]; - if (!is_array($group)) { - $ok = false; - $error = "'{$claim}' should be an array"; - } else if (array_key_exists($mapping['claim'], - $group)) { - $value = $group[$mapping['claim']]; - } + $params['oauth_consumer_key'] = $claims['iss']; + foreach (lti_get_jwt_claim_mapping() as $key => $mapping) { + $claim = LTI_JWT_CLAIM_PREFIX; + if (!empty($mapping['suffix'])) { + $claim .= "-{$mapping['suffix']}"; } - if (!empty($value) && $mapping['isarray']) { - if (!is_array($value)) { - $ok = false; - $error = "'{$value}' should be an array"; - } else if (is_array($value[0])) { - $value = json_encode($value); + $claim .= '/claim/'; + if (is_null($mapping['group'])) { + $claim = $mapping['claim']; + } else if (empty($mapping['group'])) { + $claim .= $mapping['claim']; + } else { + $claim .= $mapping['group']; + } + if (isset($claims[$claim])) { + $value = null; + if (empty($mapping['group'])) { + $value = $claims[$claim]; } else { - $value = implode(',', $value); + $group = $claims[$claim]; + if (is_array($group) && array_key_exists($mapping['claim'], $group)) { + $value = $group[$mapping['claim']]; + } + } + if (!empty($value) && $mapping['isarray']) { + if (is_array($value)) { + if (is_array($value[0])) { + $value = json_encode($value); + } else { + $value = implode(',', $value); + } + } + } + if (!is_null($value) && is_string($value) && (strlen($value) > 0)) { + $params[$key] = $value; } } - if (!is_null($value) && is_string($value) && (strlen($value) > 0)) { - $params[$key] = $value; - } - } - } - if ($ok) { - $claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom'; - if (isset($claims[$claim])) { - $custom = $claims[$claim]; - if (!is_array($custom)) { - $ok = false; - $error = "'{$custom}' should be an array"; - } else { - foreach ($custom as $key => $value) { - $params["custom_{$key}"] = $value; + $claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom'; + if (isset($claims[$claim])) { + $custom = $claims[$claim]; + if (is_array($custom)) { + foreach ($custom as $key => $value) { + $params["custom_{$key}"] = $value; + } } } - } - } - if ($ok) { - $claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext'; - if (isset($claims[$claim])) { - $ext = $claims[$claim]; - if (!is_array($ext)) { - $ok = false; - $error = "'{$ext}' should be an array"; - } else { - foreach ($ext as $key => $value) { - $params["ext_{$key}"] = $value; + $claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext'; + if (isset($claims[$claim])) { + $ext = $claims[$claim]; + if (is_array($ext)) { + foreach ($ext as $key => $value) { + $params["ext_{$key}"] = $value; + } } } } @@ -3273,8 +3281,8 @@ function lti_post_launch_html($newparms, $endpoint, $debug=false) { * @param string $text Description of content item * @return string */ -function lti_initiatelogin($courseid, $id, $instance, $config, $messagetype = 'basic-lti-launch-request', $title = '', $text = '') { - global $SESSION, $USER, $COURSE; +function lti_initiate_login($courseid, $id, $instance, $config, $messagetype = 'basic-lti-launch-request', $title = '', $text = '') { + global $SESSION, $USER; if (!empty($instance)) { $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl; @@ -3287,7 +3295,7 @@ function lti_initiatelogin($courseid, $id, $instance, $config, $messagetype = 'b $endpoint = trim($endpoint); // If SSL is forced make sure https is on the normal launch URL. - if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) { + if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) { $endpoint = lti_ensure_url_is_https($endpoint); } else if (!strstr($endpoint, '://')) { $endpoint = 'http://' . $endpoint; diff --git a/mod/lti/view.php b/mod/lti/view.php index e186f6cb039..06bc4521028 100644 --- a/mod/lti/view.php +++ b/mod/lti/view.php @@ -141,7 +141,7 @@ if (($launchcontainer == LTI_LAUNCH_CONTAINER_WINDOW) && } else { $content = ''; if ($config->lti_ltiversion === LTI_VERSION_1P3) { - $content = lti_initiatelogin($cm->course, $id, $lti, $config); + $content = lti_initiate_login($cm->course, $id, $lti, $config); } // Request the launch content with an iframe tag.