Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

   1  <?php
   2  
   3  namespace Packback\Lti1p3;
   4  
   5  use Packback\Lti1p3\Interfaces\ICache;
   6  use Packback\Lti1p3\Interfaces\ICookie;
   7  use Packback\Lti1p3\Interfaces\IDatabase;
   8  
   9  class LtiOidcLogin
  10  {
  11      public const COOKIE_PREFIX = 'lti1p3_';
  12      public const ERROR_MSG_LAUNCH_URL = 'No launch URL configured';
  13      public const ERROR_MSG_ISSUER = 'Could not find issuer';
  14      public const ERROR_MSG_LOGIN_HINT = 'Could not find login hint';
  15      private $db;
  16      private $cache;
  17      private $cookie;
  18  
  19      /**
  20       * Constructor.
  21       *
  22       * @param  IDatabase  $database Instance of the Database interface used for looking up registrations and deployments
  23       * @param  ICache  $cache    instance of the Cache interface used to loading and storing launches
  24       * @param  ICookie  $cookie   instance of the Cookie interface used to set and read cookies
  25       */
  26      public function __construct(IDatabase $database, ICache $cache = null, ICookie $cookie = null)
  27      {
  28          $this->db = $database;
  29          $this->cache = $cache;
  30          $this->cookie = $cookie;
  31      }
  32  
  33      /**
  34       * Static function to allow for method chaining without having to assign to a variable first.
  35       */
  36      public static function new(IDatabase $database, ICache $cache = null, ICookie $cookie = null)
  37      {
  38          return new LtiOidcLogin($database, $cache, $cookie);
  39      }
  40  
  41      /**
  42       * Calculate the redirect location to return to based on an OIDC third party initiated login request.
  43       *
  44       * @param  string  $launch_url URL to redirect back to after the OIDC login. This URL must match exactly a URL white listed in the platform.
  45       * @param  array|string  $request    An array of request parameters. If not set will default to $_REQUEST.
  46       * @return Redirect returns a redirect object containing the fully formed OIDC login URL
  47       */
  48      public function doOidcLoginRedirect($launch_url, array $request = null)
  49      {
  50          if ($request === null) {
  51              $request = $_REQUEST;
  52          }
  53  
  54          if (empty($launch_url)) {
  55              throw new OidcException(static::ERROR_MSG_LAUNCH_URL, 1);
  56          }
  57  
  58          // Validate Request Data.
  59          $registration = $this->validateOidcLogin($request);
  60  
  61          /*
  62           * Build OIDC Auth Response.
  63           */
  64  
  65          // Generate State.
  66          // Set cookie (short lived)
  67          $state = static::secureRandomString('state-');
  68          $this->cookie->setCookie(static::COOKIE_PREFIX.$state, $state, 60);
  69  
  70          // Generate Nonce.
  71          $nonce = static::secureRandomString('nonce-');
  72          $this->cache->cacheNonce($nonce, $state);
  73  
  74          // Build Response.
  75          $auth_params = [
  76              'scope' => 'openid', // OIDC Scope.
  77              'response_type' => 'id_token', // OIDC response is always an id token.
  78              'response_mode' => 'form_post', // OIDC response is always a form post.
  79              'prompt' => 'none', // Don't prompt user on redirect.
  80              'client_id' => $registration->getClientId(), // Registered client id.
  81              'redirect_uri' => $launch_url, // URL to return to after login.
  82              'state' => $state, // State to identify browser session.
  83              'nonce' => $nonce, // Prevent replay attacks.
  84              'login_hint' => $request['login_hint'], // Login hint to identify platform session.
  85          ];
  86  
  87          // Pass back LTI message hint if we have it.
  88          if (isset($request['lti_message_hint'])) {
  89              // LTI message hint to identify LTI context within the platform.
  90              $auth_params['lti_message_hint'] = $request['lti_message_hint'];
  91          }
  92  
  93          $auth_login_return_url = $registration->getAuthLoginUrl().'?'.http_build_query($auth_params, '', '&');
  94  
  95          // Return auth redirect.
  96          return new Redirect($auth_login_return_url, http_build_query($request, '', '&'));
  97      }
  98  
  99      public function validateOidcLogin($request)
 100      {
 101          // Validate Issuer.
 102          if (empty($request['iss'])) {
 103              throw new OidcException(static::ERROR_MSG_ISSUER, 1);
 104          }
 105  
 106          // Validate Login Hint.
 107          if (empty($request['login_hint'])) {
 108              throw new OidcException(static::ERROR_MSG_LOGIN_HINT, 1);
 109          }
 110  
 111          // Fetch Registration Details.
 112          $clientId = $request['client_id'] ?? null;
 113          $registration = $this->db->findRegistrationByIssuer($request['iss'], $clientId);
 114  
 115          // Check we got something.
 116          if (empty($registration)) {
 117              $errorMsg = LtiMessageLaunch::getMissingRegistrationErrorMsg($request['iss'], $clientId);
 118  
 119              throw new OidcException($errorMsg, 1);
 120          }
 121  
 122          // Return Registration.
 123          return $registration;
 124      }
 125  
 126      public static function secureRandomString(string $prefix = ''): string
 127      {
 128          return $prefix.hash('sha256', random_bytes(64));
 129      }
 130  }