Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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  // This file is part of BasicLTI4Moodle
  18  //
  19  // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability)
  20  // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web
  21  // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI
  22  // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS
  23  // are already supporting or going to support BasicLTI. This project Implements the consumer
  24  // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas.
  25  // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem
  26  // at the GESSI research group at UPC.
  27  // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI
  28  // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a
  29  // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier.
  30  //
  31  // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis
  32  // of the Universitat Politecnica de Catalunya http://www.upc.edu
  33  // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu.
  34  
  35  namespace mod_lti\local\ltiopenid;
  36  
  37  /**
  38   * OpenId LTI Registration library tests
  39   *
  40   * @package    mod_lti
  41   * @copyright  2020 Claude Vervoort, Cengage
  42   * @author     Claude Vervoort
  43   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  44   */
  45  class registration_test extends \advanced_testcase {
  46  
  47      /**
  48       * @var string A has-it-all client registration.
  49       */
  50      private $registrationfulljson = <<<EOD
  51      {
  52          "application_type": "web",
  53          "response_types": ["id_token"],
  54          "grant_types": ["implict", "client_credentials"],
  55          "initiate_login_uri": "https://client.example.org/lti/init",
  56          "redirect_uris":
  57          ["https://client.example.org/callback",
  58          "https://client.example.org/callback2"],
  59          "client_name": "Virtual Garden",
  60          "client_name#ja": "バーチャルガーデン",
  61          "jwks_uri": "https://client.example.org/.well-known/jwks.json",
  62          "logo_uri": "https://client.example.org/logo.png",
  63          "policy_uri": "https://client.example.org/privacy",
  64          "policy_uri#ja": "https://client.example.org/privacy?lang=ja",
  65          "tos_uri": "https://client.example.org/tos",
  66          "tos_uri#ja": "https://client.example.org/tos?lang=ja",
  67          "token_endpoint_auth_method": "private_key_jwt",
  68          "contacts": ["ve7jtb@example.org", "mary@example.org"],
  69          "scope": "https://purl.imsglobal.org/spec/lti-ags/scope/score https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
  70          "https://purl.imsglobal.org/spec/lti-tool-configuration": {
  71              "domain": "client.example.org",
  72              "description": "Learn Botany by tending to your little (virtual) garden.",
  73              "description#ja": "小さな(仮想)庭に行くことで植物学を学びましょう。",
  74              "target_link_uri": "https://client.example.org/lti",
  75              "custom_parameters": {
  76                  "context_history": "\$Context.id.history"
  77              },
  78              "claims": ["iss", "sub", "name", "given_name", "family_name", "email"],
  79              "messages": [
  80                  {
  81                      "type": "LtiDeepLinkingRequest",
  82                      "target_link_uri": "https://client.example.org/lti/dl",
  83                      "label": "Add a virtual garden",
  84                      "label#ja": "バーチャルガーデンを追加する"
  85                  }
  86              ]
  87          }
  88      }
  89  EOD;
  90  
  91      /**
  92       * @var string A minimalist client registration.
  93       */
  94      private $registrationminimaljson = <<<EOD
  95      {
  96          "application_type": "web",
  97          "response_types": ["id_token"],
  98          "grant_types": ["implict", "client_credentials"],
  99          "initiate_login_uri": "https://client.example.org/lti/init",
 100          "redirect_uris":
 101          ["https://client.example.org/callback"],
 102          "client_name": "Virtual Garden",
 103          "jwks_uri": "https://client.example.org/.well-known/jwks.json",
 104          "token_endpoint_auth_method": "private_key_jwt",
 105          "https://purl.imsglobal.org/spec/lti-tool-configuration": {
 106              "domain": "www.example.org",
 107              "target_link_uri": "https://www.example.org/lti"
 108          }
 109      }
 110  EOD;
 111  
 112      /**
 113       * @var string A minimalist with deep linking client registration.
 114       */
 115      private $registrationminimaldljson = <<<EOD
 116      {
 117          "application_type": "web",
 118          "response_types": ["id_token"],
 119          "grant_types": ["implict", "client_credentials"],
 120          "initiate_login_uri": "https://client.example.org/lti/init",
 121          "redirect_uris":
 122          ["https://client.example.org/callback"],
 123          "client_name": "Virtual Garden",
 124          "jwks_uri": "https://client.example.org/.well-known/jwks.json",
 125          "token_endpoint_auth_method": "private_key_jwt",
 126          "https://purl.imsglobal.org/spec/lti-tool-configuration": {
 127              "domain": "client.example.org",
 128              "target_link_uri": "https://client.example.org/lti",
 129              "messages": [
 130                  {
 131                      "type": "LtiDeepLinkingRequest"
 132                  }
 133              ]
 134          }
 135      }
 136  EOD;
 137  
 138      /**
 139       * Test the mapping from Registration JSON to LTI Config for a has-it-all tool registration.
 140       */
 141      public function test_to_config_full() {
 142          $registration = json_decode($this->registrationfulljson, true);
 143          $registration['scope'] .= ' https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly';
 144          $config = registration_helper::get()->registration_to_config($registration, 'TheClientId');
 145          $this->assertEquals('JWK_KEYSET', $config->lti_keytype);
 146          $this->assertEquals(LTI_VERSION_1P3, $config->lti_ltiversion);
 147          $this->assertEquals('TheClientId', $config->lti_clientid);
 148          $this->assertEquals('Virtual Garden', $config->lti_typename);
 149          $this->assertEquals('Learn Botany by tending to your little (virtual) garden.', $config->lti_description);
 150          $this->assertEquals('https://client.example.org/lti/init', $config->lti_initiatelogin);
 151          $this->assertEquals(implode(PHP_EOL, ["https://client.example.org/callback",
 152              "https://client.example.org/callback2"]), $config->lti_redirectionuris);
 153          $this->assertEquals("context_history=\$Context.id.history", $config->lti_customparameters);
 154          $this->assertEquals("https://client.example.org/.well-known/jwks.json", $config->lti_publickeyset);
 155          $this->assertEquals("https://client.example.org/logo.png", $config->lti_icon);
 156          $this->assertEquals(2, $config->ltiservice_gradesynchronization);
 157          $this->assertEquals(LTI_SETTING_DELEGATE, $config->lti_acceptgrades);
 158          $this->assertEquals(1, $config->ltiservice_memberships);
 159          $this->assertEquals(0, $config->ltiservice_toolsettings);
 160          $this->assertEquals('client.example.org', $config->lti_tooldomain);
 161          $this->assertEquals('https://client.example.org/lti', $config->lti_toolurl);
 162          $this->assertEquals(LTI_SETTING_ALWAYS, $config->lti_sendname);
 163          $this->assertEquals(LTI_SETTING_ALWAYS, $config->lti_sendemailaddr);
 164          $this->assertEquals(1, $config->lti_contentitem);
 165          $this->assertEquals('https://client.example.org/lti/dl', $config->lti_toolurl_ContentItemSelectionRequest);
 166      }
 167  
 168      /**
 169       * Test the mapping from Registration JSON to LTI Config for a minimal tool registration.
 170       */
 171      public function test_to_config_minimal() {
 172          $registration = json_decode($this->registrationminimaljson, true);
 173          $config = registration_helper::get()->registration_to_config($registration, 'TheClientId');
 174          $this->assertEquals('JWK_KEYSET', $config->lti_keytype);
 175          $this->assertEquals(LTI_VERSION_1P3, $config->lti_ltiversion);
 176          $this->assertEquals('TheClientId', $config->lti_clientid);
 177          $this->assertEquals('Virtual Garden', $config->lti_typename);
 178          $this->assertEmpty($config->lti_description);
 179          // Special case here where Moodle ignores www for domains.
 180          $this->assertEquals('example.org', $config->lti_tooldomain);
 181          $this->assertEquals('https://www.example.org/lti', $config->lti_toolurl);
 182          $this->assertEquals('https://client.example.org/lti/init', $config->lti_initiatelogin);
 183          $this->assertEquals('https://client.example.org/callback', $config->lti_redirectionuris);
 184          $this->assertEmpty($config->lti_customparameters);
 185          $this->assertEquals("https://client.example.org/.well-known/jwks.json", $config->lti_publickeyset);
 186          $this->assertEmpty($config->lti_icon);
 187          $this->assertEquals(0, $config->ltiservice_gradesynchronization);
 188          $this->assertEquals(LTI_SETTING_NEVER, $config->lti_acceptgrades);
 189          $this->assertEquals(0, $config->ltiservice_memberships);
 190          $this->assertEquals(LTI_SETTING_NEVER, $config->lti_sendname);
 191          $this->assertEquals(LTI_SETTING_NEVER, $config->lti_sendemailaddr);
 192          $this->assertEquals(0, $config->lti_contentitem);
 193      }
 194  
 195      /**
 196       * Test the mapping from Registration JSON to LTI Config for a minimal tool with
 197       * deep linking support registration.
 198       */
 199      public function test_to_config_minimal_with_deeplinking() {
 200          $registration = json_decode($this->registrationminimaldljson, true);
 201          $config = registration_helper::get()->registration_to_config($registration, 'TheClientId');
 202          $this->assertEquals(1, $config->lti_contentitem);
 203          $this->assertEmpty($config->lti_toolurl_ContentItemSelectionRequest);
 204      }
 205  
 206      /**
 207       * Validation Test: initiation login.
 208       */
 209      public function test_validation_initlogin() {
 210          $registration = json_decode($this->registrationfulljson, true);
 211          $this->expectException(registration_exception::class);
 212          $this->expectExceptionCode(400);
 213          unset($registration['initiate_login_uri']);
 214          registration_helper::get()->registration_to_config($registration, 'TheClientId');
 215      }
 216  
 217      /**
 218       * Validation Test: redirect uris.
 219       */
 220      public function test_validation_redirecturis() {
 221          $registration = json_decode($this->registrationfulljson, true);
 222          $this->expectException(registration_exception::class);
 223          $this->expectExceptionCode(400);
 224          unset($registration['redirect_uris']);
 225          registration_helper::get()->registration_to_config($registration, 'TheClientId');
 226      }
 227  
 228      /**
 229       * Validation Test: jwks uri empty.
 230       */
 231      public function test_validation_jwks() {
 232          $registration = json_decode($this->registrationfulljson, true);
 233          $this->expectException(registration_exception::class);
 234          $this->expectExceptionCode(400);
 235          $registration['jwks_uri'] = '';
 236          registration_helper::get()->registration_to_config($registration, 'TheClientId');
 237      }
 238  
 239      /**
 240       * Validation Test: no domain nor targetlinkuri is rejected.
 241       */
 242      public function test_validation_missing_domain_targetlinkuri() {
 243          $registration = json_decode($this->registrationminimaljson, true);
 244          $this->expectException(registration_exception::class);
 245          $this->expectExceptionCode(400);
 246          unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['domain']);
 247          unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['target_link_uri']);
 248          registration_helper::get()->registration_to_config($registration, 'TheClientId');
 249      }
 250  
 251      /**
 252       * Validation Test: mismatch between domain and targetlinkuri is rejected.
 253       */
 254      public function test_validation_domain_targetlinkuri_match() {
 255          $registration = json_decode($this->registrationminimaljson, true);
 256          $this->expectException(registration_exception::class);
 257          $this->expectExceptionCode(400);
 258          $registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['domain'] = 'not.the.right.domain';
 259          registration_helper::get()->registration_to_config($registration, 'TheClientId');
 260      }
 261  
 262      /**
 263       * Validation Test: domain is required.
 264       */
 265      public function test_validation_domain_targetlinkuri_onlylink() {
 266          $registration = json_decode($this->registrationminimaljson, true);
 267          unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['domain']);
 268          $this->expectException(registration_exception::class);
 269          $this->expectExceptionCode(400);
 270          $config = registration_helper::get()->registration_to_config($registration, 'TheClientId');
 271      }
 272  
 273      /**
 274       * Validation Test: base url (targetlinkuri) is built from domain if not present.
 275       */
 276      public function test_validation_domain_targetlinkuri_onlydomain() {
 277          $registration = json_decode($this->registrationminimaljson, true);
 278          unset($registration['https://purl.imsglobal.org/spec/lti-tool-configuration']['target_link_uri']);
 279          $config = registration_helper::get()->registration_to_config($registration, 'TheClientId');
 280          $this->assertEquals('example.org', $config->lti_tooldomain);
 281          $this->assertEquals('https://www.example.org', $config->lti_toolurl);
 282      }
 283  
 284      /**
 285       * Test the transformation from lti config to OpenId LTI Client Registration response.
 286       */
 287      public function test_config_to_registration() {
 288          $orig = json_decode($this->registrationfulljson, true);
 289          $orig['scope'] .= ' https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly';
 290          $reghelper = registration_helper::get();
 291          $reg = $reghelper->config_to_registration($reghelper->registration_to_config($orig, 'clid'), 12);
 292          $this->assertEquals('clid', $reg['client_id']);
 293          $this->assertEquals($orig['response_types'], $reg['response_types']);
 294          $this->assertEquals($orig['initiate_login_uri'], $reg['initiate_login_uri']);
 295          $this->assertEquals($orig['redirect_uris'], $reg['redirect_uris']);
 296          $this->assertEquals($orig['jwks_uri'], $reg['jwks_uri']);
 297          $this->assertEquals($orig['logo_uri'], $reg['logo_uri']);
 298          $this->assertEquals('https://purl.imsglobal.org/spec/lti-ags/scope/score '.
 299              'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly '.
 300              'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly '.
 301              'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem '.
 302              'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly', $reg['scope']);
 303          $ltiorig = $orig['https://purl.imsglobal.org/spec/lti-tool-configuration'];
 304          $lti = $reg['https://purl.imsglobal.org/spec/lti-tool-configuration'];
 305          $this->assertEquals("12", $lti['deployment_id']);
 306          $this->assertEquals($ltiorig['target_link_uri'], $lti['target_link_uri']);
 307          $this->assertEquals($ltiorig['domain'], $lti['domain']);
 308          $this->assertEquals($ltiorig['custom_parameters'], $lti['custom_parameters']);
 309          $this->assertEquals($ltiorig['description'], $lti['description']);
 310          $dlmsgorig = $ltiorig['messages'][0];
 311          $dlmsg = $lti['messages'][0];
 312          $this->assertEquals($dlmsgorig['type'], $dlmsg['type']);
 313          $this->assertEquals($dlmsgorig['target_link_uri'], $dlmsg['target_link_uri']);
 314          $this->assertTrue(in_array('iss', $lti['claims']));
 315          $this->assertTrue(in_array('sub', $lti['claims']));
 316          $this->assertTrue(in_array('email', $lti['claims']));
 317          $this->assertTrue(in_array('family_name', $lti['claims']));
 318          $this->assertTrue(in_array('given_name', $lti['claims']));
 319          $this->assertTrue(in_array('name', $lti['claims']));
 320      }
 321  
 322      /**
 323       * Test the transformation from lti config to OpenId LTI Client Registration response for the minimal version.
 324       */
 325      public function test_config_to_registration_minimal() {
 326          $orig = json_decode($this->registrationminimaljson, true);
 327          $reghelper = registration_helper::get();
 328          $reg = $reghelper->config_to_registration($reghelper->registration_to_config($orig, 'clid'), 12);
 329          $this->assertEquals('clid', $reg['client_id']);
 330          $this->assertEquals($orig['response_types'], $reg['response_types']);
 331          $this->assertEquals($orig['initiate_login_uri'], $reg['initiate_login_uri']);
 332          $this->assertEquals($orig['redirect_uris'], $reg['redirect_uris']);
 333          $this->assertEquals($orig['jwks_uri'], $reg['jwks_uri']);
 334          $this->assertEquals('', $reg['scope']);
 335          $ltiorig = $orig['https://purl.imsglobal.org/spec/lti-tool-configuration'];
 336          $lti = $reg['https://purl.imsglobal.org/spec/lti-tool-configuration'];
 337          $this->assertTrue(in_array('iss', $lti['claims']));
 338          $this->assertTrue(in_array('sub', $lti['claims']));
 339          $this->assertFalse(in_array('email', $lti['claims']));
 340          $this->assertFalse(in_array('family_name', $lti['claims']));
 341          $this->assertFalse(in_array('given_name', $lti['claims']));
 342          $this->assertFalse(in_array('name', $lti['claims']));
 343      }
 344  
 345      /**
 346       * Test the transformation from lti config 1.1 to Registration Response.
 347       */
 348      public function test_config_to_registration_lti11() {
 349          $config = [];
 350          $config['contentitem'] = 1;
 351          $config['toolurl_ContentItemSelectionRequest'] = '';
 352          $config['sendname'] = 0;
 353          $config['sendemailaddr'] = 1;
 354          $config['acceptgrades'] = 2;
 355          $config['resourcekey'] = 'testkey';
 356          $config['password'] = 'testp@ssw0rd';
 357          $config['customparameters'] = 'a1=b1';
 358          $type = [];
 359          $type['id'] = 130;
 360          $type['name'] = 'LTI Test 1.1';
 361          $type['baseurl'] = 'https://base.test.url/test';
 362          $type['tooldomain'] = 'base.test.url';
 363          $type['ltiversion'] = 'LTI-1p0';
 364          $type['icon'] = 'https://base.test.url/icon.png';
 365  
 366          $reg = registration_helper::get()->config_to_registration((object)$config, $type['id'], (object)$type);
 367          $this->assertFalse(isset($reg['client_id']));
 368          $this->assertFalse(isset($reg['initiate_login_uri']));
 369          $this->assertEquals($type['name'], $reg['client_name']);
 370          $lti = $reg['https://purl.imsglobal.org/spec/lti-tool-configuration'];
 371          $this->assertEquals(LTI_VERSION_1, $lti['version']);
 372          $this->assertEquals('b1', $lti['custom_parameters']['a1']);
 373          $this->assertEquals('LtiDeepLinkingRequest', $lti['messages'][0]['type']);
 374          $this->assertEquals('base.test.url', $lti['domain']);
 375          $this->assertEquals($type['baseurl'], $lti['target_link_uri']);
 376          $oauth = $lti['oauth_consumer'];
 377          $this->assertEquals('testkey', $oauth['key']);
 378          $this->assertFalse(empty($oauth['nonce']));
 379          $this->assertEquals(hash('sha256', 'testkeytestp@ssw0rd'.$oauth['nonce']), $oauth['sign']);
 380          $this->assertTrue(in_array('iss', $lti['claims']));
 381          $this->assertTrue(in_array('sub', $lti['claims']));
 382          $this->assertTrue(in_array('email', $lti['claims']));
 383          $this->assertFalse(in_array('family_name', $lti['claims']));
 384          $this->assertFalse(in_array('given_name', $lti['claims']));
 385          $this->assertFalse(in_array('name', $lti['claims']));
 386      }
 387  
 388      /**
 389       * Test the transformation from lti config 2.0 to Registration Response.
 390       * For LTI 2.0 we limit to just passing the previous key/secret.
 391       */
 392      public function test_config_to_registration_lti20() {
 393          $config = [];
 394          $config['contentitem'] = 1;
 395          $config['toolurl_ContentItemSelectionRequest'] = '';
 396          $type = [];
 397          $type['id'] = 131;
 398          $type['name'] = 'LTI Test 1.2';
 399          $type['baseurl'] = 'https://base.test.url/test';
 400          $type['tooldomain'] = 'base.test.url';
 401          $type['ltiversion'] = 'LTI-2p0';
 402          $type['icon'] = 'https://base.test.url/icon.png';
 403          $type['toolproxyid'] = 9;
 404          $toolproxy = [];
 405          $toolproxy['id'] = 9;
 406          $toolproxy['guid'] = 'lti2guidtest';
 407          $toolproxy['secret'] = 'peM7YDx420bo';
 408  
 409          $reghelper = $this->getMockBuilder(registration_helper::class)
 410              ->setMethods(['get_tool_proxy'])
 411              ->getMock();
 412          $map = [[$toolproxy['id'], $toolproxy]];
 413          $reghelper->method('get_tool_proxy')
 414              ->will($this->returnValueMap($map));
 415          $reg = $reghelper->config_to_registration((object)$config, $type['id'], (object)$type);
 416          $this->assertFalse(isset($reg['client_id']));
 417          $this->assertFalse(isset($reg['initiate_login_uri']));
 418          $this->assertEquals($type['name'], $reg['client_name']);
 419          $lti = $reg['https://purl.imsglobal.org/spec/lti-tool-configuration'];
 420          $this->assertEquals(LTI_VERSION_2, $lti['version']);
 421          $this->assertEquals('LtiDeepLinkingRequest', $lti['messages'][0]['type']);
 422          $this->assertEquals('base.test.url', $lti['domain']);
 423          $this->assertEquals($type['baseurl'], $lti['target_link_uri']);
 424          $oauth = $lti['oauth_consumer'];
 425          $this->assertEquals('lti2guidtest', $toolproxy['guid']);
 426          $this->assertFalse(empty($oauth['nonce']));
 427          $this->assertEquals(hash('sha256', 'lti2guidtestpeM7YDx420bo'.$oauth['nonce']), $oauth['sign']);
 428          $this->assertTrue(in_array('iss', $lti['claims']));
 429          $this->assertTrue(in_array('sub', $lti['claims']));
 430          $this->assertFalse(in_array('email', $lti['claims']));
 431          $this->assertFalse(in_array('family_name', $lti['claims']));
 432          $this->assertFalse(in_array('given_name', $lti['claims']));
 433          $this->assertFalse(in_array('name', $lti['claims']));
 434      }
 435  }