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  namespace enrol_lti\local\ltiadvantage\entity;
  18  
  19  use enrol_lti\local\ltiadvantage\repository\legacy_consumer_repository;
  20  
  21  /**
  22   * The migration_claim class, instances of which represent information passed in an 'lti1p1' migration claim.
  23   *
  24   * Provides validation and data retrieval for the claim.
  25   *
  26   * See https://www.imsglobal.org/spec/lti/v1p3/migr#lti-1-1-migration-claim
  27   *
  28   * @package enrol_lti
  29   * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
  30   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  31   */
  32  class migration_claim {
  33  
  34      /** @var string the LTI 1.1 consumer key */
  35      private $consumerkey;
  36  
  37      /** @var string the LTI 1.1 user identifier.
  38       * This is only included in the claim if it differs to the value included in the LTI 1.3 'sub' claim.
  39       * I.e. https://www.imsglobal.org/spec/security/v1p0#id-token
  40       */
  41      private $userid = null;
  42  
  43      /** @var string the LTI 1.1 context identifier.
  44       * This is only included in the claim if it differs to the 'id' property of the LTI 1.3 'context' claim.
  45       * I.e. https://purl.imsglobal.org/spec/lti/claim/context#id.
  46       */
  47      private $contextid = null;
  48  
  49      /** @var string the LTI 1.1 consumer instance GUID.
  50       * This is only included in the claim if it differs to the 'guid' property of the LTI 1.3 'tool_platform' claim.
  51       * I.e. https://purl.imsglobal.org/spec/lti/claim/tool_platform#guid.
  52       */
  53      private $toolconsumerinstanceguid = null;
  54  
  55      /** @var string the LTI 1.1 resource link identifier.
  56       * This is only included in the claim if it differs to the 'id' property of the LTI 1.3 'resource_link' claim.
  57       * I.e. https://purl.imsglobal.org/spec/lti/claim/resource_link#id.
  58       */
  59      private $resourcelinkid = null;
  60  
  61      /** @var legacy_consumer_repository repository instance for querying consumer secrets when verifying signature. */
  62      private $legacyconsumerrepo;
  63  
  64      /**
  65       * The migration_claim constructor.
  66       *
  67       * @param array $claim the array of claim data, as received in a resource link launch.
  68       * @param string $deploymentid the deployment id included in the launch.
  69       * @param string $platform the platform included in the launch.
  70       * @param string $clientid the client id included in the launch.
  71       * @param string $exp the exp included in the launch.
  72       * @param string $nonce the nonce included in the launch.
  73       * @param legacy_consumer_repository $legacyconsumerrepo  a legacy consumer repository instance.
  74       * @throws \coding_exception if the claim data is invalid.
  75       */
  76      public function __construct(array $claim, string $deploymentid, string $platform, string $clientid, string $exp,
  77              string $nonce, legacy_consumer_repository $legacyconsumerrepo) {
  78  
  79          // The oauth_consumer_key property MUST be sent.
  80          // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key.
  81          if (empty($claim['oauth_consumer_key'])) {
  82              throw new \coding_exception("Missing 'oauth_consumer_key' property in lti1p1 migration claim.");
  83          }
  84  
  85          // The oauth_consumer_key_sign property MAY be sent.
  86          // For user migration to take place, however, this is deemed a required property.
  87          // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key_sign.
  88          if (empty($claim['oauth_consumer_key_sign'])) {
  89              throw new \coding_exception("Missing 'oauth_consumer_key_sign' property in lti1p1 migration claim.");
  90          }
  91          $this->legacyconsumerrepo = $legacyconsumerrepo;
  92  
  93          if (!$this->verify_signature($claim['oauth_consumer_key'], $claim['oauth_consumer_key_sign'], $deploymentid,
  94                  $platform, $clientid, $exp, $nonce, $legacyconsumerrepo)) {
  95              throw new \coding_exception("Invalid 'oauth_consumer_key_sign' signature in lti1p1 claim.");
  96          }
  97  
  98          $this->consumerkey = $claim['oauth_consumer_key'];
  99          $this->userid = $claim['user_id'] ?? null;
 100          $this->contextid = $claim['context_id'] ?? null;
 101          $this->toolconsumerinstanceguid = $claim['tool_consumer_instance_guid'] ?? null;
 102          $this->resourcelinkid = $claim['resource_link_id'] ?? null;
 103      }
 104  
 105      /**
 106       * Verify the claim signature by recalculating it using the launch data and locally stored consumer secret.
 107       *
 108       * @param string $consumerkey the LTI 1.1 consumer key.
 109       * @param string $signature a signature of the LTI 1.1 consumer key and associated launch data.
 110       * @param string $deploymentid the deployment id included in the launch.
 111       * @param string $platform the platform included in the launch.
 112       * @param string $clientid the client id included in the launch.
 113       * @param string $exp the exp included in the launch.
 114       * @param string $nonce the nonce included in the launch.
 115       * @return bool true if the signature was verified, false otherwise.
 116       */
 117      private function verify_signature(string $consumerkey, string $signature, string $deploymentid, string $platform,
 118          string $clientid, string $exp, string $nonce): bool {
 119  
 120          $base = [
 121              $consumerkey,
 122              $deploymentid,
 123              $platform,
 124              $clientid,
 125              $exp,
 126              $nonce
 127          ];
 128          $basestring = implode('&', $base);
 129  
 130          // Legacy enrol_lti code permits tools to share a consumer key but use different secrets. This results in
 131          // potentially many secrets per mapped tool consumer. As such, when generating the migration claim it's
 132          // impossible to know which secret the platform will use to sign the consumer key. The consumer key in the
 133          // migration claim is thus verified by trying all known secrets for the consumer, until either a match is found
 134          // or no signatures match.
 135          $consumersecrets = $this->legacyconsumerrepo->get_consumer_secrets($consumerkey);
 136          foreach ($consumersecrets as $consumersecret) {
 137              $calculatedsignature = base64_encode(hash_hmac('sha256', $basestring, $consumersecret));
 138  
 139              if ($signature === $calculatedsignature) {
 140                  return true;
 141              }
 142          }
 143          return false;
 144      }
 145  
 146      /**
 147       * Return the consumer key stored in the claim.
 148       *
 149       * @return string the consumer key included in the claim.
 150       */
 151      public function get_consumer_key(): string {
 152          return $this->consumerkey;
 153      }
 154  
 155      /**
 156       * Return the LTI 1.1 user id stored in the claim.
 157       *
 158       * @return string|null the user id, or null if not provided in the claim.
 159       */
 160      public function get_user_id(): ?string {
 161          return $this->userid;
 162      }
 163  
 164  
 165      /**
 166       * Return the LTI 1.1 context id stored in the claim.
 167       *
 168       * @return string|null the context id, or null if not provided in the claim.
 169       */
 170      public function get_context_id(): ?string {
 171          return $this->contextid;
 172      }
 173  
 174      /**
 175       * Return the LTI 1.1 tool consumer instance GUID stored in the claim.
 176       *
 177       * @return string|null the tool consumer instance GUID, or null if not provided in the claim.
 178       */
 179      public function get_tool_consumer_instance_guid(): ?string {
 180          return $this->toolconsumerinstanceguid;
 181      }
 182  
 183      /**
 184       * Return the LTI 1.1 resource link id stored in the claim.
 185       *
 186       * @return string|null the resource link id, or null if not provided in the claim.
 187       */
 188      public function get_resource_link_id(): ?string {
 189          return $this->resourcelinkid;
 190      }
 191  }