Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * A Helper for LTI Dynamic Registration. 19 * 20 * @package mod_lti 21 * @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill) 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 namespace mod_lti\local\ltiopenid; 25 26 defined('MOODLE_INTERNAL') || die; 27 28 require_once($CFG->dirroot . '/mod/lti/locallib.php'); 29 use Firebase\JWT\JWK; 30 use Firebase\JWT\JWT; 31 use stdClass; 32 33 /** 34 * This class exposes functions for LTI Dynamic Registration. 35 * 36 * @package mod_lti 37 * @copyright 2020 Claude Vervoort (Cengage), Carlos Costa, Adrian Hutchinson (Macgraw Hill) 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 class registration_helper { 41 /** score scope */ 42 const SCOPE_SCORE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score'; 43 /** result scope */ 44 const SCOPE_RESULT = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly'; 45 /** lineitem read-only scope */ 46 const SCOPE_LINEITEM_RO = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly'; 47 /** lineitem full access scope */ 48 const SCOPE_LINEITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'; 49 /** Names and Roles (membership) scope */ 50 const SCOPE_NRPS = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly'; 51 /** Tool Settings scope */ 52 const SCOPE_TOOL_SETTING = 'https://purl.imsglobal.org/spec/lti-ts/scope/toolsetting'; 53 54 55 /** 56 * Function used to validate parameters. 57 * 58 * This function is needed because the payload contains nested 59 * objects, and optional_param() does not support arrays of arrays. 60 * 61 * @param array $payload that may contain the parameter key 62 * @param string $key the key of the value to be looked for in the payload 63 * @param bool $required if required, not finding a value will raise a registration_exception 64 * 65 * @return mixed 66 */ 67 private static function get_parameter(array $payload, string $key, bool $required) { 68 if (!isset($payload[$key]) || empty($payload[$key])) { 69 if ($required) { 70 throw new registration_exception('missing required attribute '.$key, 400); 71 } 72 return null; 73 } 74 $parameter = $payload[$key]; 75 // Cleans parameters to avoid XSS and other issues. 76 if (is_array($parameter)) { 77 return clean_param_array($parameter, PARAM_TEXT, true); 78 } 79 return clean_param($parameter, PARAM_TEXT); 80 } 81 82 /** 83 * Transforms an LTI 1.3 Registration to a Moodle LTI Config. 84 * 85 * @param array $registrationpayload the registration data received from the tool. 86 * @param string $clientid the clientid to be issued for that tool. 87 * 88 * @return object the Moodle LTI config. 89 */ 90 public static function registration_to_config(array $registrationpayload, string $clientid): object { 91 $responsetypes = self::get_parameter($registrationpayload, 'response_types', true); 92 $initiateloginuri = self::get_parameter($registrationpayload, 'initiate_login_uri', true); 93 $redirecturis = self::get_parameter($registrationpayload, 'redirect_uris', true); 94 $clientname = self::get_parameter($registrationpayload, 'client_name', true); 95 $jwksuri = self::get_parameter($registrationpayload, 'jwks_uri', true); 96 $tokenendpointauthmethod = self::get_parameter($registrationpayload, 'token_endpoint_auth_method', true); 97 98 $applicationtype = self::get_parameter($registrationpayload, 'application_type', false); 99 $logouri = self::get_parameter($registrationpayload, 'logo_uri', false); 100 101 $ltitoolconfiguration = self::get_parameter($registrationpayload, 102 'https://purl.imsglobal.org/spec/lti-tool-configuration', true); 103 104 $domain = self::get_parameter($ltitoolconfiguration, 'domain', false); 105 $targetlinkuri = self::get_parameter($ltitoolconfiguration, 'target_link_uri', false); 106 $customparameters = self::get_parameter($ltitoolconfiguration, 'custom_parameters', false); 107 $scopes = explode(" ", self::get_parameter($registrationpayload, 'scope', false) ?? ''); 108 $claims = self::get_parameter($ltitoolconfiguration, 'claims', false); 109 $messages = $ltitoolconfiguration['messages'] ?? []; 110 $description = self::get_parameter($ltitoolconfiguration, 'description', false); 111 112 // Validate domain and target link. 113 if (empty($domain)) { 114 throw new registration_exception('missing_domain', 400); 115 } 116 $targetlinkuri = $targetlinkuri ?: 'https://'.$domain; 117 if ($domain !== lti_get_domain_from_url($targetlinkuri)) { 118 throw new registration_exception('domain_targetlinkuri_mismatch', 400); 119 } 120 121 // Validate response type. 122 // According to specification, for this scenario, id_token must be explicitly set. 123 if (!in_array('id_token', $responsetypes)) { 124 throw new registration_exception('invalid_response_types', 400); 125 } 126 127 // According to specification, this parameter needs to be an array. 128 if (!is_array($redirecturis)) { 129 throw new registration_exception('invalid_redirect_uris', 400); 130 } 131 132 // According to specification, for this scenario private_key_jwt must be explicitly set. 133 if ($tokenendpointauthmethod !== 'private_key_jwt') { 134 throw new registration_exception('invalid_token_endpoint_auth_method', 400); 135 } 136 137 if (!empty($applicationtype) && $applicationtype !== 'web') { 138 throw new registration_exception('invalid_application_type', 400); 139 } 140 141 $config = new stdClass(); 142 $config->lti_clientid = $clientid; 143 $config->lti_toolurl = $targetlinkuri; 144 $config->lti_tooldomain = $domain; 145 $config->lti_typename = $clientname; 146 $config->lti_description = $description; 147 $config->lti_ltiversion = LTI_VERSION_1P3; 148 $config->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEID; 149 $config->lti_icon = $logouri; 150 $config->lti_coursevisible = LTI_COURSEVISIBLE_PRECONFIGURED; 151 $config->lti_contentitem = 0; 152 // Sets Content Item. 153 if (!empty($messages)) { 154 $messagesresponse = []; 155 foreach ($messages as $value) { 156 if ($value['type'] === 'LtiDeepLinkingRequest') { 157 $config->lti_contentitem = 1; 158 $config->lti_toolurl_ContentItemSelectionRequest = $value['target_link_uri'] ?? ''; 159 array_push($messagesresponse, $value); 160 } 161 } 162 } 163 164 $config->lti_keytype = 'JWK_KEYSET'; 165 $config->lti_publickeyset = $jwksuri; 166 $config->lti_initiatelogin = $initiateloginuri; 167 $config->lti_redirectionuris = implode(PHP_EOL, $redirecturis); 168 $config->lti_customparameters = ''; 169 // Sets custom parameters. 170 if (isset($customparameters)) { 171 $paramssarray = []; 172 foreach ($customparameters as $key => $value) { 173 array_push($paramssarray, $key . '=' . $value); 174 } 175 $config->lti_customparameters = implode(PHP_EOL, $paramssarray); 176 } 177 // Sets launch container. 178 $config->lti_launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS; 179 180 // Sets Service info based on scopes. 181 $config->lti_acceptgrades = LTI_SETTING_NEVER; 182 $config->ltiservice_gradesynchronization = 0; 183 $config->ltiservice_memberships = 0; 184 $config->ltiservice_toolsettings = 0; 185 if (isset($scopes)) { 186 // Sets Assignment and Grade Services info. 187 188 if (in_array(self::SCOPE_SCORE, $scopes)) { 189 $config->lti_acceptgrades = LTI_SETTING_DELEGATE; 190 $config->ltiservice_gradesynchronization = 1; 191 } 192 if (in_array(self::SCOPE_RESULT, $scopes)) { 193 $config->lti_acceptgrades = LTI_SETTING_DELEGATE; 194 $config->ltiservice_gradesynchronization = 1; 195 } 196 if (in_array(self::SCOPE_LINEITEM_RO, $scopes)) { 197 $config->lti_acceptgrades = LTI_SETTING_DELEGATE; 198 $config->ltiservice_gradesynchronization = 1; 199 } 200 if (in_array(self::SCOPE_LINEITEM, $scopes)) { 201 $config->lti_acceptgrades = LTI_SETTING_DELEGATE; 202 $config->ltiservice_gradesynchronization = 2; 203 } 204 205 // Sets Names and Role Provisioning info. 206 if (in_array(self::SCOPE_NRPS, $scopes)) { 207 $config->ltiservice_memberships = 1; 208 } 209 210 // Sets Tool Settings info. 211 if (in_array(self::SCOPE_TOOL_SETTING, $scopes)) { 212 $config->ltiservice_toolsettings = 1; 213 } 214 } 215 216 // Sets privacy settings. 217 $config->lti_sendname = LTI_SETTING_NEVER; 218 $config->lti_sendemailaddr = LTI_SETTING_NEVER; 219 if (isset($claims)) { 220 // Sets name privacy settings. 221 222 if (in_array('name', $claims)) { 223 $config->lti_sendname = LTI_SETTING_ALWAYS; 224 } 225 if (in_array('given_name', $claims)) { 226 $config->lti_sendname = LTI_SETTING_ALWAYS; 227 } 228 if (in_array('family_name', $claims)) { 229 $config->lti_sendname = LTI_SETTING_ALWAYS; 230 } 231 232 // Sets email privacy settings. 233 if (in_array('email', $claims)) { 234 $config->lti_sendemailaddr = LTI_SETTING_ALWAYS; 235 } 236 } 237 return $config; 238 } 239 240 /** 241 * Transforms a moodle LTI 1.3 Config to an OAuth/LTI Client Registration. 242 * 243 * @param object $config Moodle LTI Config. 244 * @param int $typeid which is the LTI deployment id. 245 * 246 * @return array the Client Registration as an associative array. 247 */ 248 public static function config_to_registration(object $config, int $typeid): array { 249 $registrationresponse = []; 250 $registrationresponse['client_id'] = $config->lti_clientid; 251 $registrationresponse['token_endpoint_auth_method'] = ['private_key_jwt']; 252 $registrationresponse['response_types'] = ['id_token']; 253 $registrationresponse['jwks_uri'] = $config->lti_publickeyset; 254 $registrationresponse['initiate_login_uri'] = $config->lti_initiatelogin; 255 $registrationresponse['grant_types'] = ['client_credentials', 'implicit']; 256 $registrationresponse['redirect_uris'] = explode(PHP_EOL, $config->lti_redirectionuris); 257 $registrationresponse['application_type'] = 'web'; 258 $registrationresponse['token_endpoint_auth_method'] = 'private_key_jwt'; 259 $registrationresponse['client_name'] = $config->lti_typename; 260 $registrationresponse['logo_uri'] = $config->lti_icon ?? ''; 261 $lticonfigurationresponse = []; 262 $lticonfigurationresponse['deployment_id'] = strval($typeid); 263 $lticonfigurationresponse['target_link_uri'] = $config->lti_toolurl; 264 $lticonfigurationresponse['domain'] = $config->lti_tooldomain ?? ''; 265 $lticonfigurationresponse['description'] = $config->lti_description ?? ''; 266 if ($config->lti_contentitem == 1) { 267 $contentitemmessage = []; 268 $contentitemmessage['type'] = 'LtiDeepLinkingRequest'; 269 if (isset($config->lti_toolurl_ContentItemSelectionRequest)) { 270 $contentitemmessage['target_link_uri'] = $config->lti_toolurl_ContentItemSelectionRequest; 271 } 272 $lticonfigurationresponse['messages'] = [$contentitemmessage]; 273 } 274 if (isset($config->lti_customparameters) && !empty($config->lti_customparameters)) { 275 $params = []; 276 foreach (explode(PHP_EOL, $config->lti_customparameters) as $param) { 277 $split = explode('=', $param); 278 $params[$split[0]] = $split[1]; 279 } 280 $lticonfigurationresponse['custom_parameters'] = $params; 281 } 282 $scopesresponse = []; 283 if ($config->ltiservice_gradesynchronization > 0) { 284 $scopesresponse[] = self::SCOPE_SCORE; 285 $scopesresponse[] = self::SCOPE_RESULT; 286 $scopesresponse[] = self::SCOPE_LINEITEM_RO; 287 } 288 if ($config->ltiservice_gradesynchronization == 2) { 289 $scopesresponse[] = self::SCOPE_LINEITEM; 290 } 291 if ($config->ltiservice_memberships == 1) { 292 $scopesresponse[] = self::SCOPE_NRPS; 293 } 294 if ($config->ltiservice_toolsettings == 1) { 295 $scopesresponse[] = self::SCOPE_TOOL_SETTING; 296 } 297 $registrationresponse['scope'] = implode(' ', $scopesresponse); 298 299 $claimsresponse = ['sub', 'iss']; 300 if ($config->lti_sendname == LTI_SETTING_ALWAYS) { 301 $claimsresponse[] = 'name'; 302 $claimsresponse[] = 'family_name'; 303 $claimsresponse[] = 'given_name'; 304 } 305 if ($config->lti_sendemailaddr == LTI_SETTING_ALWAYS) { 306 $claimsresponse[] = 'email'; 307 } 308 $lticonfigurationresponse['claims'] = $claimsresponse; 309 $registrationresponse['https://purl.imsglobal.org/spec/lti-tool-configuration'] = $lticonfigurationresponse; 310 return $registrationresponse; 311 } 312 313 /** 314 * Validates the registration token is properly signed and not used yet. 315 * Return the client id to use for this registration. 316 * 317 * @param string $registrationtokenjwt registration token 318 * 319 * @return string client id for the registration 320 */ 321 public static function validate_registration_token(string $registrationtokenjwt): string { 322 global $DB; 323 $keys = JWK::parseKeySet(jwks_helper::get_jwks()); 324 $registrationtoken = JWT::decode($registrationtokenjwt, $keys, ['RS256']); 325 326 // Get clientid from registrationtoken. 327 $clientid = $registrationtoken->sub; 328 329 // Checks if clientid is already registered. 330 if (!empty($DB->get_record('lti_types', array('clientid' => $clientid)))) { 331 throw new registration_exception("token_already_used", 401); 332 } 333 return $clientid; 334 } 335 336 /** 337 * Initializes an array with the scopes for services supported by the LTI module 338 * 339 * @return array List of scopes 340 */ 341 public static function lti_get_service_scopes() { 342 343 $services = lti_get_services(); 344 $scopes = array(); 345 foreach ($services as $service) { 346 $servicescopes = $service->get_scopes(); 347 if (!empty($servicescopes)) { 348 $scopes = array_merge($scopes, $servicescopes); 349 } 350 } 351 return $scopes; 352 } 353 354 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body