Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.
   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 auth_lti\local\ltiadvantage\entity;
  18  
  19  /**
  20   * A simplified representation of a 'https://purl.imsglobal.org/spec/lti/claim/lti1p1' migration claim.
  21   *
  22   * This serves the purpose of migrating a legacy user account only. Claim properties that do not relate to user migration are not
  23   * included or handled by this representation.
  24   *
  25   * See https://www.imsglobal.org/spec/lti/v1p3/migr#lti-1-1-migration-claim
  26   *
  27   * @package auth_lti
  28   * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
  29   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   */
  31  class user_migration_claim {
  32  
  33      /** @var string the LTI 1.1 consumer key */
  34      private $consumerkey;
  35  
  36      /** @var string the LTI 1.1 user identifier.
  37       * This is only included in the claim if it differs to the value included in the LTI 1.3 'sub' claim.
  38       * If not included, the value will be taken from 'sub'.
  39       */
  40      private $userid;
  41  
  42      /**
  43       * The migration_claim constructor.
  44       *
  45       * The signature of a migration claim must be verifiable. To achieve this, the constructor takes a list of secrets
  46       * corresponding to the 'oauth_consumer_key' provided in the 'https://purl.imsglobal.org/spec/lti/claim/lti1p1'
  47       * claim. How these secrets are determined is not the responsibility of this class. The constructor assumes these
  48       * correspond.
  49       *
  50       * @param array $jwt the array of claim data, as received in a resource link launch JWT.
  51       * @param array $consumersecrets a list of consumer secrets for the consumerkey included in the migration claim.
  52       * @throws \coding_exception if the claim data is invalid.
  53       */
  54      public function __construct(array $jwt, array $consumersecrets) {
  55          // Can't get a claim instance without the claim data.
  56          if (empty($jwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1'])) {
  57              throw new \coding_exception("Missing the 'https://purl.imsglobal.org/spec/lti/claim/lti1p1' JWT claim");
  58          }
  59          $claim = $jwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1'];
  60  
  61          // The oauth_consumer_key property MUST be sent.
  62          // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key.
  63          if (empty($claim['oauth_consumer_key'])) {
  64              throw new \coding_exception("Missing 'oauth_consumer_key' property in lti1p1 migration claim.");
  65          }
  66  
  67          // The oauth_consumer_key_sign property MAY be sent.
  68          // For user migration to take place, however, this is deemed a required property since Moodle identified its
  69          // legacy users through a combination of consumerkey and userid.
  70          // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key_sign.
  71          if (empty($claim['oauth_consumer_key_sign'])) {
  72              throw new \coding_exception("Missing 'oauth_consumer_key_sign' property in lti1p1 migration claim.");
  73          }
  74  
  75          if (!$this->verify_signature(
  76              $claim['oauth_consumer_key'],
  77              $claim['oauth_consumer_key_sign'],
  78              $jwt['https://purl.imsglobal.org/spec/lti/claim/deployment_id'],
  79              $jwt['iss'],
  80              $jwt['aud'],
  81              $jwt['exp'],
  82              $jwt['nonce'],
  83              $consumersecrets
  84          )) {
  85              throw new \coding_exception("Invalid 'oauth_consumer_key_sign' signature in lti1p1 claim.");
  86          }
  87  
  88          $this->consumerkey = $claim['oauth_consumer_key'];
  89          $this->userid = $claim['user_id'] ?? $jwt['sub'];
  90      }
  91  
  92      /**
  93       * Verify the claim signature by recalculating it using the launch data and cross-checking consumer secrets.
  94       *
  95       * @param string $consumerkey the LTI 1.1 consumer key.
  96       * @param string $signature a signature of the LTI 1.1 consumer key and associated launch data.
  97       * @param string $deploymentid the deployment id included in the launch.
  98       * @param string $platform the platform included in the launch.
  99       * @param string $clientid the client id included in the launch.
 100       * @param string $exp the exp included in the launch.
 101       * @param string $nonce the nonce included in the launch.
 102       * @param array $consumersecrets the list of consumer secrets used with the given $consumerkey param
 103       * @return bool true if the signature was verified, false otherwise.
 104       */
 105      private function verify_signature(string $consumerkey, string $signature, string $deploymentid, string $platform,
 106          string $clientid, string $exp, string $nonce, array $consumersecrets): bool {
 107  
 108          $base = [
 109              $consumerkey,
 110              $deploymentid,
 111              $platform,
 112              $clientid,
 113              $exp,
 114              $nonce
 115          ];
 116          $basestring = implode('&', $base);
 117  
 118          // Legacy enrol_lti code permits tools to share a consumer key but use different secrets. This results in
 119          // potentially many secrets per mapped tool consumer. As such, when generating the migration claim it's
 120          // impossible to know which secret the platform will use to sign the consumer key. The consumer key in the
 121          // migration claim is thus verified by trying all known secrets for the consumer, until either a match is found
 122          // or no signatures match.
 123          foreach ($consumersecrets as $consumersecret) {
 124              $calculatedsignature = base64_encode(hash_hmac('sha256', $basestring, $consumersecret));
 125  
 126              if ($signature === $calculatedsignature) {
 127                  return true;
 128              }
 129          }
 130          return false;
 131      }
 132  
 133      /**
 134       * Return the consumer key stored in the claim.
 135       *
 136       * @return string the consumer key included in the claim.
 137       */
 138      public function get_consumer_key(): string {
 139          return $this->consumerkey;
 140      }
 141  
 142      /**
 143       * Return the LTI 1.1 user id stored in the claim.
 144       *
 145       * @return string the user id, or null if not provided in the claim.
 146       */
 147      public function get_user_id(): string {
 148          return $this->userid;
 149      }
 150  }